Add spec files #5
@@ -53,7 +53,8 @@ jobs:
|
||||
|
||||
- name: Build Windows executable
|
||||
run: |
|
||||
poetry run pyinstaller --onefile --windowed --icon=icon_media/robot_files_256.ico --name android-file-handler src/main.py
|
||||
# Use the Windows spec file so packaging is consistent and reproducible
|
||||
poetry run pyinstaller scripts/spec_scripts/android-file-handler-windows.spec
|
||||
|
||||
- name: Upload Windows artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -101,7 +102,8 @@ jobs:
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
poetry env use python3.12 || true
|
||||
poetry install
|
||||
poetry run pyinstaller --onefile --icon=icon_media/robot_files_256.png --name android-file-handler src/main.py
|
||||
# Use the Linux spec file for reproducible builds
|
||||
poetry run pyinstaller scripts/spec_scripts/android-file-handler-linux.spec
|
||||
|
||||
- name: Package .deb (fpm)
|
||||
run: |
|
||||
@@ -159,7 +161,8 @@ jobs:
|
||||
run: |
|
||||
export PATH="$HOME/.local/bin:/usr/local/bin:$PATH"
|
||||
poetry install
|
||||
poetry run pyinstaller --onefile --icon=icon_media/robot_files_256.png --name android-file-handler src/main.py
|
||||
# Use the Linux spec file for reproducible builds on Arch
|
||||
poetry run pyinstaller scripts/spec_scripts/android-file-handler-linux.spec
|
||||
|
||||
- name: Package pacman (fpm)
|
||||
run: |
|
||||
@@ -216,127 +219,4 @@ jobs:
|
||||
draft: false
|
||||
prerelease: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# build-rocky:
|
||||
# runs-on: ubuntu-latest
|
||||
# container:
|
||||
# image: rockylinux:9
|
||||
# steps:
|
||||
# - name: Robust dnf setup and install build deps (retry-clean-refresh)
|
||||
# run: |
|
||||
# set -euo pipefail
|
||||
# retry() {
|
||||
# local max_tries=5
|
||||
# local sleep_base=3
|
||||
# local n=1
|
||||
# until "$@"; do
|
||||
# if [ $n -ge $max_tries ]; then
|
||||
# echo "Command failed after $n attempts: $*"
|
||||
# return 1
|
||||
# fi
|
||||
# echo "Command failed: $*. Retrying ($n/$max_tries) after $((sleep_base * n))s..."
|
||||
# sleep $((sleep_base * n))
|
||||
# n=$((n + 1))
|
||||
# done
|
||||
# return 0
|
||||
# }
|
||||
|
||||
# # Ensure plugin and enable CRB
|
||||
# retry dnf -y install dnf-plugins-core
|
||||
# retry dnf config-manager --set-enabled crb || true
|
||||
|
||||
# # Clean caches and refresh metadata
|
||||
# retry dnf -y clean all
|
||||
# rm -rf /var/cache/dnf || true
|
||||
# retry dnf -y makecache --refresh
|
||||
|
||||
# # Install build-time deps including ruby so gem stdlib is present
|
||||
# retry dnf -y install -y \
|
||||
# git gcc make openssl-devel bzip2-devel libffi-devel zlib-devel xz-devel \
|
||||
# readline-devel sqlite-devel tk-devel gdbm-devel libuuid-devel patch \
|
||||
# ruby ruby-devel rubygems rpm-build tar ca-certificates wget
|
||||
|
||||
# - name: Clean up pyenv
|
||||
# run: |
|
||||
# echo "Removing existing pyenv installations"
|
||||
# rm -rf "${GITHUB_WORKSPACE}/.pyenv"
|
||||
# rm -rf "${HOME}/.pyenv"
|
||||
|
||||
# - name: Tarball-based pyenv + python-build (non-interactive)
|
||||
# env:
|
||||
# PYENV_ROOT: ${{ github.workspace }}/.pyenv # Set this BEFORE running installer
|
||||
# run: |
|
||||
# set -euo pipefail
|
||||
|
||||
# echo "PYENV_ROOT is set to: $PYENV_ROOT"
|
||||
# mkdir -p "$PYENV_ROOT"
|
||||
|
||||
# echo "Downloading pyenv installer and running it"
|
||||
# wget -qO- https://pyenv.run | bash
|
||||
|
||||
# # Initialize pyenv environment for this job
|
||||
# export PATH="$PYENV_ROOT/bin:$PATH"
|
||||
# eval "$($PYENV_ROOT/bin/pyenv init -)"
|
||||
|
||||
# # Persist environment variables for subsequent steps
|
||||
# echo "PYENV_ROOT=$PYENV_ROOT" >> $GITHUB_ENV
|
||||
# echo "PATH=$PYENV_ROOT/shims:$PYENV_ROOT/bin:$PATH" >> $GITHUB_ENV
|
||||
|
||||
# echo "pyenv version:"
|
||||
# "$PYENV_ROOT/bin/pyenv" --version || true
|
||||
|
||||
# - name: Build Python with pyenv (3.12.11) and set global
|
||||
# env:
|
||||
# PYENV_ROOT: ${{ github.workspace }}/.pyenv
|
||||
# run: |
|
||||
# set -euo pipefail
|
||||
# export PYENV_ROOT="${PYENV_ROOT}"
|
||||
# export PATH="$PYENV_ROOT/bin:$PATH"
|
||||
# eval "$($PYENV_ROOT/bin/pyenv init -)"
|
||||
# # Install python (idempotent)
|
||||
# $PYENV_ROOT/bin/pyenv install -s 3.12.11
|
||||
# $PYENV_ROOT/bin/pyenv global 3.12.11
|
||||
# python --version
|
||||
# pip --version
|
||||
|
||||
# - name: Install Poetry
|
||||
# uses: snok/install-poetry@v1
|
||||
# with:
|
||||
# version: latest
|
||||
# virtualenvs-create: true
|
||||
# virtualenvs-in-project: true
|
||||
|
||||
# - name: Configure Poetry
|
||||
# run: |
|
||||
# echo 'export PATH="$HOME/.local/bin:$PATH"' >> $GITHUB_ENV
|
||||
# export PATH="$HOME/.local/bin:$PATH"
|
||||
# poetry config virtualenvs.create true
|
||||
# poetry config virtualenvs.in-project true
|
||||
|
||||
# - name: Install dependencies & build executable
|
||||
# run: |
|
||||
# set -euo pipefail
|
||||
# export PATH="$HOME/.local/bin:$PYENV_ROOT/shims:$PYENV_ROOT/bin:$PATH"
|
||||
# poetry env use 3.12.11 || true
|
||||
# poetry install
|
||||
# poetry run pyinstaller --onefile --icon=icon_media/robot_files_256.png --name android-file-handler src/main.py
|
||||
|
||||
# - name: Install fpm and package .rpm
|
||||
# run: |
|
||||
# set -euo pipefail
|
||||
# # install pinned fpm and ensure binary is in system bindir
|
||||
# gem install --no-document -v "${FPM_VERSION}" fpm --bindir /usr/local/bin
|
||||
# echo "/usr/local/bin" >> $GITHUB_PATH
|
||||
|
||||
# # Build rpm with explicit filename
|
||||
# VERSION="$(poetry version -s)"
|
||||
# mkdir -p dist
|
||||
# chmod +x dist/android-file-handler
|
||||
# fpm -s dir -t rpm -n android-file-handler -v "$VERSION" --architecture x86_64 --prefix /usr/local/bin -p "dist/android-file-handler-${VERSION}-1.x86_64.rpm" dist/android-file-handler
|
||||
|
||||
# - name: Upload Rocky RPM
|
||||
# uses: actions/upload-artifact@v4
|
||||
# with:
|
||||
# name: rocky-package
|
||||
# path: dist/*.rpm
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -7,6 +7,8 @@ build
|
||||
.actrc
|
||||
bin/
|
||||
test-act.yml
|
||||
pkg_dist
|
||||
|
||||
# Python cache files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
@@ -18,7 +18,7 @@ A simple Windows GUI tool to pull folders from an Android device using ADB (Andr
|
||||
- [Poetry](https://python-poetry.org/) for dependency management
|
||||
- (Optional) pyenv for Python version managment
|
||||
|
||||
## Installation
|
||||
## Installation (Developers)
|
||||
|
||||
1. Clone this repository:
|
||||
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
# Arch Linux installation instructions
|
||||
|
||||
## Arch Linux installation
|
||||
|
||||
These instructions describe how Arch users can install the Linux build produced by the project's `simple-release.yml` workflow. The workflow produces an Arch package named similar to:
|
||||
|
||||
```bash
|
||||
android-file-handler-<VERSION>-1-x86_64.pkg.tar.zst
|
||||
```
|
||||
|
||||
Find that file on the project's GitHub Releases page for the matching version and download it.
|
||||
|
||||
### System install (recommended)
|
||||
|
||||
Use pacman to install the downloaded package so package scripts run and files are placed in standard system locations:
|
||||
|
||||
```bash
|
||||
# from the directory with the downloaded package
|
||||
sudo pacman -U ./android-file-handler-1.2.3-1-x86_64.pkg.tar.zst
|
||||
```
|
||||
|
||||
Replace the filename with the actual release artifact you downloaded. After installation you should have:
|
||||
|
||||
- Binary: e.g. `/usr/local/bin/android-file-handler`
|
||||
- Desktop entry: `/usr/share/applications/android-file-handler.desktop`
|
||||
- Icons: under `/usr/share/icons/hicolor/...`
|
||||
|
||||
Launch the app from your application menu (search "Android File Handler") or from a terminal:
|
||||
|
||||
```bash
|
||||
android-file-handler
|
||||
```
|
||||
|
||||
### Non-root / manual install
|
||||
|
||||
If you cannot or do not want to install the package system-wide, you can extract and install files into your home directory manually:
|
||||
|
||||
1. Extract the package to a temporary folder:
|
||||
|
||||
```bash
|
||||
mkdir -p /tmp/afh_pkg
|
||||
tar -I zstd -xvf android-file-handler-1.2.3-1-x86_64.pkg.tar.zst -C /tmp/afh_pkg
|
||||
```
|
||||
|
||||
2. Copy the executable and desktop/icon files to your local directories:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.local/bin ~/.local/share/applications ~/.local/share/icons/hicolor/256x256/apps
|
||||
cp /tmp/afh_pkg/usr/local/bin/android-file-handler ~/.local/bin/
|
||||
chmod +x ~/.local/bin/android-file-handler
|
||||
|
||||
# Copy desktop entry and icon (paths inside the package may vary)
|
||||
cp /tmp/afh_pkg/usr/share/applications/android-file-handler.desktop ~/.local/share/applications/
|
||||
cp /tmp/afh_pkg/usr/share/icons/hicolor/256x256/apps/android-file-handler.png ~/.local/share/icons/hicolor/256x256/apps/
|
||||
```
|
||||
|
||||
3. (Optional) Update desktop and icon caches so the menu picks up the new entry immediately:
|
||||
|
||||
```bash
|
||||
update-desktop-database ~/.local/share/applications || true
|
||||
gtk-update-icon-cache -t -f ~/.local/share/icons/hicolor || true
|
||||
```
|
||||
|
||||
Now run the app from the menu or with `~/.local/bin/android-file-handler`.
|
||||
|
||||
### Verify package contents
|
||||
|
||||
To inspect the package contents without installing:
|
||||
|
||||
```bash
|
||||
# list files in the package
|
||||
pacman -Qlp android-file-handler-1.2.3-1-x86_64.pkg.tar.zst
|
||||
```
|
||||
|
||||
To query package info (after installation):
|
||||
|
||||
```bash
|
||||
pacman -Qi android-file-handler
|
||||
```
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
- If the app does not appear in your menu, try logging out/in or run the cache update commands above.
|
||||
- If icons are missing, ensure the `icons/hicolor/*/apps/android-file-handler.png` entries were copied to the correct theme directories.
|
||||
- If you manually copied files, ensure `~/.local/bin` is on your PATH; add it to `~/.profile` if necessary:
|
||||
|
||||
```bash
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
```
|
||||
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"]
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/bin/bash
|
||||
poetry lock
|
||||
poetry install
|
||||
poetry run pyinstaller --onefile src/main.py --distpath dist --name android-file-handler-windows
|
||||
Executable
+75
@@ -0,0 +1,75 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
poetry lock
|
||||
poetry install
|
||||
|
||||
# Build using the Linux spec so the build is reproducible and uses the project spec
|
||||
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"
|
||||
VERSION="$(poetry version -s)"
|
||||
mkdir -p dist
|
||||
chmod +x dist/android-file-handler
|
||||
# explicit filename into dist/ (Debian .deb)
|
||||
# change directory into dist and package the binary named 'android-file-handler'
|
||||
# this ensures the package installs /usr/local/bin/android-file-handler (no extra dist/ prefix)
|
||||
|
||||
# Debian
|
||||
# Prepare packaging layout in a separate directory to avoid clobbering build artifacts
|
||||
PKG_DIR="pkg_dist"
|
||||
rm -rf "$PKG_DIR"
|
||||
mkdir -p "$PKG_DIR/usr/local/bin" \
|
||||
"$PKG_DIR/usr/share/applications" \
|
||||
"$PKG_DIR/usr/share/icons/hicolor/256x256/apps"
|
||||
|
||||
# Copy built binary
|
||||
cp dist/android-file-handler "$PKG_DIR/usr/local/bin/android-file-handler"
|
||||
chmod 0755 "$PKG_DIR/usr/local/bin/android-file-handler"
|
||||
|
||||
# ...existing code...
|
||||
# Copy icon if present in repo (fallback optional) — do this before creating the .desktop
|
||||
ICON_SRC="icon_media/robot_files_256.png"
|
||||
ICON_DST="$PKG_DIR/usr/share/icons/hicolor/256x256/apps/android-file-handler.png"
|
||||
ICON_INCLUDED=false
|
||||
if [ -f "$ICON_SRC" ]; then
|
||||
mkdir -p "$(dirname "$ICON_DST")"
|
||||
cp "$ICON_SRC" "$ICON_DST"
|
||||
chmod 644 "$ICON_DST"
|
||||
ICON_INCLUDED=true
|
||||
else
|
||||
echo "Warning: icon not found at $ICON_SRC; packaging without icon"
|
||||
fi
|
||||
|
||||
# Create .desktop file (icon name matches installed icon)
|
||||
cat > "$PKG_DIR/usr/share/applications/android-file-handler.desktop" <<'EOF'
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Android File Handler
|
||||
Comment=Manage Android device files via ADB
|
||||
Exec=/usr/local/bin/android-file-handler %U
|
||||
Icon=android-file-handler
|
||||
Terminal=false
|
||||
Categories=Utility;Development;
|
||||
StartupNotify=true
|
||||
EOF
|
||||
chmod 644 "$PKG_DIR/usr/share/applications/android-file-handler.desktop"
|
||||
|
||||
# Build the Debian package and include postinst script
|
||||
PKG_ITEMS=( "usr/local/bin/android-file-handler" "usr/share/applications/android-file-handler.desktop" )
|
||||
if [ "$ICON_INCLUDED" = true ]; then
|
||||
PKG_ITEMS+=( "usr/share/icons/hicolor/256x256/apps/android-file-handler.png" )
|
||||
fi
|
||||
|
||||
# Debug listing
|
||||
echo "Packaging the following items (relative to $PKG_DIR):"
|
||||
printf ' - %s\n' "${PKG_ITEMS[@]}"
|
||||
for p in "${PKG_ITEMS[@]}"; do ls -la "$PKG_DIR/$p" || echo " (missing) $PKG_DIR/$p"; done
|
||||
|
||||
fpm -s dir -t deb -n android-file-handler -v "$VERSION" \
|
||||
--architecture amd64 --deb-user root --deb-group root \
|
||||
--after-install scripts/linux_postinst.sh \
|
||||
-p "dist/android-file-handler_${VERSION}_amd64.deb" -C "$PKG_DIR" "${PKG_ITEMS[@]}"
|
||||
|
||||
echo "Package created at dist/android-file-handler_${VERSION}_amd64.deb"
|
||||
Executable
+26
@@ -0,0 +1,26 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Post-installation script for android-file-handler
|
||||
# - update desktop database if available
|
||||
# - update icon cache if available
|
||||
# - ensure binary is executable
|
||||
|
||||
# Update desktop database if the utility exists
|
||||
if command -v update-desktop-database >/dev/null 2>&1; then
|
||||
update-desktop-database /usr/share/applications || true
|
||||
fi
|
||||
|
||||
# Update GTK icon cache for hicolor theme if utility exists
|
||||
if command -v gtk-update-icon-cache >/dev/null 2>&1; then
|
||||
if [ -d /usr/share/icons/hicolor ]; then
|
||||
gtk-update-icon-cache -t -f /usr/share/icons/hicolor || true
|
||||
fi
|
||||
fi
|
||||
|
||||
# Ensure installed binary is executable
|
||||
if [ -f /usr/local/bin/android-file-handler ]; then
|
||||
chmod 0755 /usr/local/bin/android-file-handler || true
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -0,0 +1,49 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
block_cipher = None
|
||||
a = Analysis(
|
||||
['../../src/main.py'],
|
||||
pathex=['src'],
|
||||
binaries=[],
|
||||
datas=[
|
||||
('../../src/gui', 'gui'),
|
||||
('../../scripts/linux_postinst.sh', 'scripts'),
|
||||
],
|
||||
hiddenimports=[
|
||||
'gui',
|
||||
'gui.file_browser',
|
||||
'gui.license_agreement',
|
||||
'gui.main_window',
|
||||
'gui.progress_handler',
|
||||
'adb_manager'
|
||||
],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
noarchive=False,
|
||||
)
|
||||
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
[],
|
||||
name='android-file-handler',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=False,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
)
|
||||
+13
-10
@@ -1,15 +1,18 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
|
||||
block_cipher = None
|
||||
|
||||
|
||||
a = Analysis(
|
||||
['src/main.py'],
|
||||
pathex=[],
|
||||
pathex=['src'],
|
||||
binaries=[],
|
||||
datas=[],
|
||||
hiddenimports=[],
|
||||
datas=[('src/gui', 'gui'), ('../scripts/windows/first_run_install.ps1', 'scripts/windows')],
|
||||
hiddenimports=[
|
||||
'gui',
|
||||
'gui.file_browser',
|
||||
'gui.license_agreement',
|
||||
'gui.main_window',
|
||||
'gui.progress_handler'
|
||||
'adb_manager'
|
||||
],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
@@ -20,7 +23,6 @@ a = Analysis(
|
||||
noarchive=False,
|
||||
)
|
||||
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
@@ -35,10 +37,11 @@ exe = EXE(
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=True,
|
||||
console=False,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
)
|
||||
icon='icon_media/robot_files_256.ico',
|
||||
)
|
||||
@@ -0,0 +1,51 @@
|
||||
param(
|
||||
[string]$ExePath = "$PSScriptRoot\..\..\dist\android-file-handler.exe",
|
||||
[string]$IconPath = "$PSScriptRoot\..\..\assets\icons\android-file-handler.ico",
|
||||
[string]$AppName = "Android File Handler"
|
||||
)
|
||||
|
||||
function Ensure-Elevated {
|
||||
if (-not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
|
||||
# Relaunch the script with elevation
|
||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||
$psi.FileName = "powershell.exe"
|
||||
$psi.Arguments = "-ExecutionPolicy Bypass -File `"$PSCommandPath`""
|
||||
$psi.Verb = "runas"
|
||||
try {
|
||||
[System.Diagnostics.Process]::Start($psi) | Out-Null
|
||||
Exit 0
|
||||
} catch {
|
||||
Write-Error "Elevation required to install to Program Files."
|
||||
Exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ensure-Elevated
|
||||
|
||||
$destDir = Join-Path ${env:ProgramFiles} $AppName
|
||||
if (-not (Test-Path $destDir)) { New-Item -ItemType Directory -Path $destDir | Out-Null }
|
||||
|
||||
$resolvedExe = Resolve-Path -Path $ExePath -ErrorAction SilentlyContinue
|
||||
if (-not $resolvedExe) {
|
||||
Write-Error "Application executable not found at $ExePath"
|
||||
Exit 1
|
||||
}
|
||||
|
||||
Copy-Item -Path $resolvedExe -Destination (Join-Path $destDir (Split-Path $resolvedExe -Leaf)) -Force
|
||||
|
||||
# Create Start Menu shortcut
|
||||
$programs = Join-Path $env:APPDATA 'Microsoft\Windows\Start Menu\Programs'
|
||||
$appFolder = Join-Path $programs $AppName
|
||||
if (-not (Test-Path $appFolder)) { New-Item -ItemType Directory -Path $appFolder | Out-Null }
|
||||
|
||||
$shortcutPath = Join-Path $appFolder "$AppName.lnk"
|
||||
$wsh = New-Object -ComObject WScript.Shell
|
||||
$sc = $wsh.CreateShortcut($shortcutPath)
|
||||
$sc.TargetPath = (Join-Path $destDir (Split-Path $resolvedExe -Leaf))
|
||||
$sc.WorkingDirectory = $destDir
|
||||
if (Test-Path $IconPath) { $sc.IconLocation = Resolve-Path $IconPath }
|
||||
$sc.Save()
|
||||
|
||||
Write-Output "Installed $AppName to $destDir and created Start Menu shortcut."
|
||||
Exit 0
|
||||
+197
-126
@@ -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."""
|
||||
@@ -23,6 +26,140 @@ def get_executable_directory() -> str:
|
||||
else:
|
||||
# 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."""
|
||||
@@ -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.")
|
||||
return True
|
||||
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,76 @@ import os
|
||||
import sys
|
||||
import tkinter as tk
|
||||
from tkinter import messagebox, scrolledtext
|
||||
import tempfile
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import os
|
||||
|
||||
|
||||
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:
|
||||
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:
|
||||
# Running as script - use directory containing the main script
|
||||
# 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
|
||||
@@ -73,12 +107,48 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE."""
|
||||
|
||||
|
||||
def resource_path(relative_path: str) -> str:
|
||||
"""Return absolute path to resource for dev and frozen runs."""
|
||||
try:
|
||||
if getattr(sys, "frozen", False):
|
||||
base = getattr(sys, "_MEIPASS", os.path.dirname(sys.executable))
|
||||
else:
|
||||
base = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
return os.path.normpath(os.path.join(base, relative_path))
|
||||
except Exception:
|
||||
return os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), relative_path))
|
||||
|
||||
|
||||
def run_windows_first_run_if_needed() -> None:
|
||||
"""If running on Windows and license not agreed, launch first-run installer script.
|
||||
|
||||
Uses `resource_path` to locate the bundled PowerShell script in both dev and frozen modes.
|
||||
"""
|
||||
try:
|
||||
if not sys.platform.startswith("win"):
|
||||
return
|
||||
|
||||
if check_license_agreement():
|
||||
return
|
||||
|
||||
script_rel = os.path.join("scripts", "windows", "first_run_install.ps1")
|
||||
script_path = resource_path(script_rel)
|
||||
if not os.path.exists(script_path):
|
||||
return
|
||||
try:
|
||||
subprocess.Popen(["powershell.exe", "-ExecutionPolicy", "Bypass", "-File", script_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
class LicenseAgreementFrame(tk.Frame):
|
||||
"""License agreement UI frame that can be embedded in the main window."""
|
||||
|
||||
|
||||
def __init__(self, parent, on_agree_callback):
|
||||
"""Initialize the license agreement frame.
|
||||
|
||||
|
||||
Args:
|
||||
parent: Parent widget
|
||||
on_agree_callback: Function to call when user agrees to license
|
||||
|
||||
+3
-6
@@ -4,13 +4,10 @@ Android File Handler - Main Entry Point
|
||||
Simple entry point to launch the Android file transfer application.
|
||||
"""
|
||||
|
||||
try:
|
||||
# Try relative import first (when used as module)
|
||||
from .gui.main_window import main
|
||||
except ImportError:
|
||||
# Fall back to direct import (when run directly)
|
||||
from gui.main_window import main
|
||||
from gui.main_window import main
|
||||
from gui.license_agreement import run_windows_first_run_if_needed
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_windows_first_run_if_needed()
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user