Add spec files #5
@@ -53,7 +53,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Build Windows executable
|
- name: Build Windows executable
|
||||||
run: |
|
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
|
- name: Upload Windows artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
@@ -101,7 +102,8 @@ jobs:
|
|||||||
export PATH="$HOME/.local/bin:$PATH"
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
poetry env use python3.12 || true
|
poetry env use python3.12 || true
|
||||||
poetry install
|
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)
|
- name: Package .deb (fpm)
|
||||||
run: |
|
run: |
|
||||||
@@ -159,7 +161,8 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
export PATH="$HOME/.local/bin:/usr/local/bin:$PATH"
|
export PATH="$HOME/.local/bin:/usr/local/bin:$PATH"
|
||||||
poetry install
|
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)
|
- name: Package pacman (fpm)
|
||||||
run: |
|
run: |
|
||||||
@@ -216,127 +219,4 @@ jobs:
|
|||||||
draft: false
|
draft: false
|
||||||
prerelease: false
|
prerelease: false
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
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
|
|
||||||
@@ -7,6 +7,8 @@ build
|
|||||||
.actrc
|
.actrc
|
||||||
bin/
|
bin/
|
||||||
test-act.yml
|
test-act.yml
|
||||||
|
pkg_dist
|
||||||
|
|
||||||
# Python cache files
|
# Python cache files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.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
|
- [Poetry](https://python-poetry.org/) for dependency management
|
||||||
- (Optional) pyenv for Python version managment
|
- (Optional) pyenv for Python version managment
|
||||||
|
|
||||||
## Installation
|
## Installation (Developers)
|
||||||
|
|
||||||
1. Clone this repository:
|
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]]
|
[[package]]
|
||||||
name = "platformdirs"
|
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`."
|
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.9"
|
||||||
groups = ["dev"]
|
groups = ["main", "dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"},
|
{file = "platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85"},
|
||||||
{file = "platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc"},
|
{file = "platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
@@ -909,4 +909,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.1"
|
lock-version = "2.1"
|
||||||
python-versions = "<3.13,>=3.12"
|
python-versions = "<3.13,>=3.12"
|
||||||
content-hash = "dfb6152cb0fb34dcda59387fbaef4f445b0f38437bd7bdfe25b38b62b14d527e"
|
content-hash = "2e5b4bc609ab9efe7fe2f83c7c475b1189133759463452d0ed664870cea7ed08"
|
||||||
|
|||||||
+2
-1
@@ -8,11 +8,12 @@ authors = [
|
|||||||
license = { text = "MIT" }
|
license = { text = "MIT" }
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = "<3.13,>=3.12"
|
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]
|
[tool.poetry.dependencies]
|
||||||
python = "<3.13,>=3.12"
|
python = "<3.13,>=3.12"
|
||||||
requests = ">=2.32.4,<3.0.0"
|
requests = ">=2.32.4,<3.0.0"
|
||||||
|
platformdirs = ">4.0.0,<5.0.0"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
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 -*-
|
# -*- mode: python ; coding: utf-8 -*-
|
||||||
|
|
||||||
|
|
||||||
block_cipher = None
|
block_cipher = None
|
||||||
|
|
||||||
|
|
||||||
a = Analysis(
|
a = Analysis(
|
||||||
['src/main.py'],
|
['src/main.py'],
|
||||||
pathex=[],
|
pathex=['src'],
|
||||||
binaries=[],
|
binaries=[],
|
||||||
datas=[],
|
datas=[('src/gui', 'gui'), ('../scripts/windows/first_run_install.ps1', 'scripts/windows')],
|
||||||
hiddenimports=[],
|
hiddenimports=[
|
||||||
|
'gui',
|
||||||
|
'gui.file_browser',
|
||||||
|
'gui.license_agreement',
|
||||||
|
'gui.main_window',
|
||||||
|
'gui.progress_handler'
|
||||||
|
'adb_manager'
|
||||||
|
],
|
||||||
hookspath=[],
|
hookspath=[],
|
||||||
hooksconfig={},
|
hooksconfig={},
|
||||||
runtime_hooks=[],
|
runtime_hooks=[],
|
||||||
@@ -20,7 +23,6 @@ a = Analysis(
|
|||||||
noarchive=False,
|
noarchive=False,
|
||||||
)
|
)
|
||||||
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
||||||
|
|
||||||
exe = EXE(
|
exe = EXE(
|
||||||
pyz,
|
pyz,
|
||||||
a.scripts,
|
a.scripts,
|
||||||
@@ -35,10 +37,11 @@ exe = EXE(
|
|||||||
upx=True,
|
upx=True,
|
||||||
upx_exclude=[],
|
upx_exclude=[],
|
||||||
runtime_tmpdir=None,
|
runtime_tmpdir=None,
|
||||||
console=True,
|
console=False,
|
||||||
disable_windowed_traceback=False,
|
disable_windowed_traceback=False,
|
||||||
argv_emulation=False,
|
argv_emulation=False,
|
||||||
target_arch=None,
|
target_arch=None,
|
||||||
codesign_identity=None,
|
codesign_identity=None,
|
||||||
entitlements_file=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 time
|
||||||
import re
|
import re
|
||||||
from typing import Optional, Tuple, Callable
|
from typing import Optional, Tuple, Callable
|
||||||
|
import tempfile
|
||||||
|
from typing import Optional
|
||||||
|
import hashlib
|
||||||
|
|
||||||
def get_executable_directory() -> str:
|
def get_executable_directory() -> str:
|
||||||
"""Get the directory containing the executable or script."""
|
"""Get the directory containing the executable or script."""
|
||||||
@@ -23,6 +26,140 @@ def get_executable_directory() -> str:
|
|||||||
else:
|
else:
|
||||||
# Running as script - use the script's directory
|
# Running as script - use the script's directory
|
||||||
return os.path.dirname(os.path.abspath(__file__))
|
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:
|
def get_platform_tools_directory() -> str:
|
||||||
"""Get platform-tools directory."""
|
"""Get platform-tools directory."""
|
||||||
@@ -60,7 +197,6 @@ ADB_WIN_ZIP_URL = (
|
|||||||
ADB_LINUX_ZIP_URL = (
|
ADB_LINUX_ZIP_URL = (
|
||||||
"https://dl.google.com/android/repository/platform-tools-latest-linux.zip"
|
"https://dl.google.com/android/repository/platform-tools-latest-linux.zip"
|
||||||
)
|
)
|
||||||
LOCAL_ADB_FOLDER = get_platform_tools_directory()
|
|
||||||
OS_TYPE = sys.platform
|
OS_TYPE = sys.platform
|
||||||
|
|
||||||
# Determine ADB binary name based on platform
|
# Determine ADB binary name based on platform
|
||||||
@@ -71,7 +207,28 @@ elif OS_TYPE.startswith("win"):
|
|||||||
else:
|
else:
|
||||||
ADB_BINARY_NAME = "adb"
|
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:
|
class ADBManager:
|
||||||
@@ -103,60 +260,39 @@ class ADBManager:
|
|||||||
def check_local_disk_space(self) -> bool:
|
def check_local_disk_space(self) -> bool:
|
||||||
"""Check if there's enough disk space for ADB download."""
|
"""Check if there's enough disk space for ADB download."""
|
||||||
try:
|
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
|
if free_space < 50 * 1024 * 1024: # 50MB minimum
|
||||||
raise Exception("Insufficient disk space")
|
raise Exception("Insufficient disk space")
|
||||||
return True
|
return True
|
||||||
except OSError:
|
except OSError:
|
||||||
# Create directory if it doesn't exist
|
# 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
|
return True
|
||||||
|
|
||||||
def download_and_extract_adb(self) -> bool:
|
def download_and_extract_adb(self) -> bool:
|
||||||
"""Download and extract ADB tools if not present."""
|
"""Download and extract ADB tools if not present."""
|
||||||
if os.path.isfile(ADB_BINARY_PATH):
|
# Use the centralized installer which will return the adb path (and
|
||||||
return True
|
# perform a download if needed). If it returns a valid path, report
|
||||||
|
# success; otherwise return False.
|
||||||
try:
|
try:
|
||||||
# Determine the correct URL based on platform
|
adb_path = ensure_platform_tools_in_user_dir()
|
||||||
if OS_TYPE.startswith("linux"):
|
if adb_path and os.path.isfile(adb_path):
|
||||||
adb_zip_url = ADB_LINUX_ZIP_URL
|
# Ensure executable permissions on POSIX
|
||||||
elif OS_TYPE.startswith("win"):
|
if os.name == "posix":
|
||||||
adb_zip_url = ADB_WIN_ZIP_URL
|
try:
|
||||||
else:
|
os.chmod(adb_path, 0o755)
|
||||||
print("Unsupported platform")
|
except Exception:
|
||||||
return False
|
pass
|
||||||
|
self._update_status("ADB available at: " + adb_path)
|
||||||
if not self.check_local_disk_space():
|
return True
|
||||||
return False
|
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
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Failed to download ADB: {e}")
|
self._update_status(f"Failed to ensure platform-tools: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def run_adb_command(self, args: list, capture_output: bool = True):
|
def run_adb_command(self, args: list, capture_output: bool = True):
|
||||||
"""Run an ADB command and return output."""
|
"""Run an ADB command and return output."""
|
||||||
cmd = [ADB_BINARY_PATH] + args
|
cmd = [get_adb_binary_path()] + args
|
||||||
try:
|
try:
|
||||||
if capture_output:
|
if capture_output:
|
||||||
p = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
|
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:
|
def pull_folder(self, remote_path: str, local_path: str) -> bool:
|
||||||
"""Pull files from Android device to local machine."""
|
"""Pull files from Android device to local machine."""
|
||||||
# Normalize paths for better compatibility
|
# Normalize paths and prepare
|
||||||
local_path = os.path.normpath(local_path)
|
local_path = os.path.normpath(local_path)
|
||||||
remote_path = remote_path.strip()
|
remote_path = remote_path.strip()
|
||||||
|
|
||||||
# Ensure local directory exists
|
|
||||||
try:
|
try:
|
||||||
os.makedirs(local_path, exist_ok=True)
|
os.makedirs(local_path, exist_ok=True)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._update_status(f"Failed to create local directory: {e}")
|
self._update_status(f"Failed to create local directory: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# For Windows root drives, ensure proper formatting
|
# Warn if writing to root drive on Windows
|
||||||
if os.name == "nt":
|
if os.name == "nt":
|
||||||
# Check if this is a root drive (like C:\, D:\, etc.)
|
|
||||||
normalized_path = os.path.abspath(local_path)
|
normalized_path = os.path.abspath(local_path)
|
||||||
drive_root = os.path.splitdrive(normalized_path)[0] + os.sep
|
drive_root = os.path.splitdrive(normalized_path)[0] + os.sep
|
||||||
if normalized_path == drive_root:
|
if normalized_path == drive_root:
|
||||||
# Root drive path like C:\ - this might cause issues with ADB
|
|
||||||
self._update_status(
|
self._update_status(
|
||||||
"Warning: Transferring to root drive. Consider using a subfolder."
|
"Warning: Transferring to root drive. Consider using a subfolder."
|
||||||
)
|
)
|
||||||
|
|
||||||
cmd = [ADB_BINARY_PATH, "pull", remote_path, local_path]
|
cmd = [get_adb_binary_path(), "pull", remote_path, local_path]
|
||||||
|
|
||||||
# Debug output for troubleshooting
|
|
||||||
self._update_status(f"Command: adb pull '{remote_path}' '{local_path}'")
|
self._update_status(f"Command: adb pull '{remote_path}' '{local_path}'")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Start with initial progress
|
|
||||||
self._update_progress(0)
|
self._update_progress(0)
|
||||||
self._update_status("Starting transfer...")
|
self._update_status("Starting transfer...")
|
||||||
|
|
||||||
@@ -276,45 +406,29 @@ class ADBManager:
|
|||||||
pct = self.parse_progress(line)
|
pct = self.parse_progress(line)
|
||||||
|
|
||||||
if pct is not None:
|
if pct is not None:
|
||||||
# Use explicit progress when available
|
|
||||||
self._update_progress(pct)
|
self._update_progress(pct)
|
||||||
last_progress = pct
|
last_progress = pct
|
||||||
last_update_time = current_time
|
last_update_time = current_time
|
||||||
else:
|
else:
|
||||||
# Improved progress estimation for large transfers
|
|
||||||
elapsed_time = current_time - start_time
|
elapsed_time = current_time - start_time
|
||||||
time_since_last_update = current_time - last_update_time
|
time_since_last_update = current_time - last_update_time
|
||||||
|
|
||||||
# Calculate progress based on multiple factors
|
|
||||||
should_update = False
|
should_update = False
|
||||||
new_progress = last_progress
|
new_progress = last_progress
|
||||||
|
|
||||||
# Time-based progress (update every 2 seconds)
|
|
||||||
if time_since_last_update >= 2.0 and last_progress < 95:
|
if time_since_last_update >= 2.0 and last_progress < 95:
|
||||||
# Estimate progress based on activity and time
|
|
||||||
if line_count > 100:
|
if line_count > 100:
|
||||||
# For large transfers, use a logarithmic approach
|
activity_factor = min(line_count / 1000, 50)
|
||||||
activity_factor = min(
|
time_factor = min(elapsed_time / 60, 40)
|
||||||
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)
|
new_progress = min(activity_factor + time_factor, 95)
|
||||||
else:
|
else:
|
||||||
# For smaller transfers, use the original approach
|
|
||||||
new_progress = min(last_progress + 10, 95)
|
new_progress = min(last_progress + 10, 95)
|
||||||
|
|
||||||
should_update = True
|
should_update = True
|
||||||
|
|
||||||
# Line-based progress (for very active transfers)
|
|
||||||
elif line_count % 50 == 0 and last_progress < 90:
|
elif line_count % 50 == 0 and last_progress < 90:
|
||||||
# More conservative line-based updates
|
|
||||||
increment = max(1, min(5, 90 // (line_count // 50 + 1)))
|
increment = max(1, min(5, 90 // (line_count // 50 + 1)))
|
||||||
new_progress = min(last_progress + increment, 90)
|
new_progress = min(last_progress + increment, 90)
|
||||||
should_update = True
|
should_update = True
|
||||||
|
|
||||||
# Update progress if needed
|
|
||||||
if should_update and new_progress > last_progress:
|
if should_update and new_progress > last_progress:
|
||||||
self._update_progress(int(new_progress))
|
self._update_progress(int(new_progress))
|
||||||
last_progress = new_progress
|
last_progress = new_progress
|
||||||
@@ -329,19 +443,17 @@ class ADBManager:
|
|||||||
self.current_process = None
|
self.current_process = None
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
# Capture error output for better debugging
|
|
||||||
error_msg = f"Transfer failed with code {proc.returncode}"
|
error_msg = f"Transfer failed with code {proc.returncode}"
|
||||||
if hasattr(proc, "stderr") and proc.stderr:
|
if hasattr(proc, "stderr") and proc.stderr:
|
||||||
try:
|
try:
|
||||||
stderr_output = proc.stderr.read()
|
stderr_output = proc.stderr.read()
|
||||||
if stderr_output:
|
if stderr_output:
|
||||||
error_msg += f". Error: {stderr_output}"
|
error_msg += f". Error: {stderr_output}"
|
||||||
except:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
self._update_status(error_msg)
|
self._update_status(error_msg)
|
||||||
self.current_process = None
|
self.current_process = None
|
||||||
return False
|
return False
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._update_status(f"Transfer error: {e}")
|
self._update_status(f"Transfer error: {e}")
|
||||||
self.current_process = None
|
self.current_process = None
|
||||||
@@ -349,33 +461,25 @@ class ADBManager:
|
|||||||
|
|
||||||
def push_folder(self, local_path: str, remote_path: str) -> bool:
|
def push_folder(self, local_path: str, remote_path: str) -> bool:
|
||||||
"""Push files from local machine to Android device."""
|
"""Push files from local machine to Android device."""
|
||||||
# Normalize paths for better compatibility
|
|
||||||
local_path = os.path.normpath(local_path)
|
local_path = os.path.normpath(local_path)
|
||||||
remote_path = remote_path.strip()
|
remote_path = remote_path.strip()
|
||||||
|
|
||||||
# Validate local path exists
|
|
||||||
if not os.path.exists(local_path):
|
if not os.path.exists(local_path):
|
||||||
self._update_status(f"Local path does not exist: {local_path}")
|
self._update_status(f"Local path does not exist: {local_path}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# For Windows root drives, ensure proper formatting
|
|
||||||
if os.name == "nt":
|
if os.name == "nt":
|
||||||
# Check if this is a root drive (like C:\, D:\, etc.)
|
|
||||||
normalized_path = os.path.abspath(local_path)
|
normalized_path = os.path.abspath(local_path)
|
||||||
drive_root = os.path.splitdrive(normalized_path)[0] + os.sep
|
drive_root = os.path.splitdrive(normalized_path)[0] + os.sep
|
||||||
if normalized_path == drive_root:
|
if normalized_path == drive_root:
|
||||||
# Root drive path like C:\ - this might cause issues with ADB
|
|
||||||
self._update_status(
|
self._update_status(
|
||||||
"Warning: Pushing from root drive. Consider using a subfolder."
|
"Warning: Pushing from root drive. Consider using a subfolder."
|
||||||
)
|
)
|
||||||
|
|
||||||
cmd = [ADB_BINARY_PATH, "push", local_path, remote_path]
|
cmd = [get_adb_binary_path(), "push", local_path, remote_path]
|
||||||
|
|
||||||
# Debug output for troubleshooting
|
|
||||||
self._update_status(f"Command: adb push '{local_path}' '{remote_path}'")
|
self._update_status(f"Command: adb push '{local_path}' '{remote_path}'")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Start with initial progress
|
|
||||||
self._update_progress(0)
|
self._update_progress(0)
|
||||||
self._update_status("Starting transfer...")
|
self._update_status("Starting transfer...")
|
||||||
|
|
||||||
@@ -403,46 +507,13 @@ class ADBManager:
|
|||||||
pct = self.parse_progress(line)
|
pct = self.parse_progress(line)
|
||||||
|
|
||||||
if pct is not None:
|
if pct is not None:
|
||||||
# Use explicit progress when available
|
|
||||||
self._update_progress(pct)
|
self._update_progress(pct)
|
||||||
last_progress = pct
|
last_progress = pct
|
||||||
last_update_time = current_time
|
last_update_time = current_time
|
||||||
else:
|
else:
|
||||||
# Improved progress estimation for large transfers
|
|
||||||
elapsed_time = current_time - start_time
|
elapsed_time = current_time - start_time
|
||||||
time_since_last_update = current_time - last_update_time
|
if elapsed_time >= 1.0 and last_progress < 90:
|
||||||
|
new_progress = min(last_progress + 20, 90)
|
||||||
# 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:
|
|
||||||
self._update_progress(int(new_progress))
|
self._update_progress(int(new_progress))
|
||||||
last_progress = new_progress
|
last_progress = new_progress
|
||||||
last_update_time = current_time
|
last_update_time = current_time
|
||||||
@@ -456,14 +527,13 @@ class ADBManager:
|
|||||||
self.current_process = None
|
self.current_process = None
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
# Capture error output for better debugging
|
|
||||||
error_msg = f"Push failed with code {proc.returncode}"
|
error_msg = f"Push failed with code {proc.returncode}"
|
||||||
if hasattr(proc, "stderr") and proc.stderr:
|
if hasattr(proc, "stderr") and proc.stderr:
|
||||||
try:
|
try:
|
||||||
stderr_output = proc.stderr.read()
|
stderr_output = proc.stderr.read()
|
||||||
if stderr_output:
|
if stderr_output:
|
||||||
error_msg += f". Error: {stderr_output}"
|
error_msg += f". Error: {stderr_output}"
|
||||||
except:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
self._update_status(error_msg)
|
self._update_status(error_msg)
|
||||||
self.current_process = None
|
self.current_process = None
|
||||||
@@ -495,7 +565,7 @@ class ADBManager:
|
|||||||
"Warning: Transferring to root drive. Consider using a subfolder."
|
"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
|
# Debug output for troubleshooting
|
||||||
self._update_status(f"Command: adb pull '{remote_file_path}' '{local_file_path}'")
|
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."
|
"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
|
# Debug output for troubleshooting
|
||||||
self._update_status(f"Command: adb push '{local_file_path}' '{remote_file_path}'")
|
self._update_status(f"Command: adb push '{local_file_path}' '{remote_file_path}'")
|
||||||
@@ -681,6 +751,8 @@ class ADBManager:
|
|||||||
return False
|
return False
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# (Method intentionally removed - use top-level ensure_platform_tools_in_user_dir)
|
||||||
|
|
||||||
|
|
||||||
class LinuxMTPManager:
|
class LinuxMTPManager:
|
||||||
"""Manages MTP operations on Linux systems."""
|
"""Manages MTP operations on Linux systems."""
|
||||||
@@ -808,14 +880,13 @@ class LinuxMTPManager:
|
|||||||
|
|
||||||
|
|
||||||
# Helper functions for standalone usage
|
# 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:
|
def is_adb_available() -> bool:
|
||||||
"""Check if ADB binary is available."""
|
"""Check if ADB binary is available using the centralized resolver."""
|
||||||
return os.path.isfile(ADB_BINARY_PATH)
|
try:
|
||||||
|
adb_path = get_adb_binary_path()
|
||||||
|
return os.path.isfile(adb_path)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def get_platform_type() -> str:
|
def get_platform_type() -> str:
|
||||||
|
|||||||
@@ -7,42 +7,76 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import messagebox, scrolledtext
|
from tkinter import messagebox, scrolledtext
|
||||||
|
import tempfile
|
||||||
|
import stat
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
def get_license_file_path():
|
def get_license_file_path() -> str:
|
||||||
"""Get the path to the license agreement file."""
|
"""Return the per-user license-agreed file path.
|
||||||
# Get the directory where the application is running from
|
|
||||||
if getattr(sys, 'frozen', False):
|
Uses platform-appropriate user config directory when possible:
|
||||||
# Running as executable
|
- On Linux/macOS this will typically use ~/.config or ~/.android-file-handler fallback.
|
||||||
app_dir = os.path.dirname(sys.executable)
|
- 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:
|
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__)))
|
app_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
return os.path.join(app_dir, "dev-mode_license_agreed.ini")
|
||||||
return os.path.join(app_dir, "license_agreed.ini")
|
|
||||||
|
|
||||||
|
|
||||||
def check_license_agreement():
|
def check_license_agreement() -> bool:
|
||||||
"""Check if user has already agreed to the license."""
|
"""Check if user has already agreed to the license."""
|
||||||
license_file = get_license_file_path()
|
license_file = get_license_file_path()
|
||||||
try:
|
try:
|
||||||
if os.path.exists(license_file):
|
if os.path.exists(license_file):
|
||||||
with open(license_file, 'r') as f:
|
with open(license_file, "r") as f:
|
||||||
content = f.read().strip()
|
return f.read().strip() == "1"
|
||||||
return content == "1"
|
|
||||||
return False
|
return False
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def save_license_agreement():
|
def save_license_agreement() -> bool:
|
||||||
"""Save that the user has agreed to the license."""
|
"""Persist that the user agreed to the license.
|
||||||
|
|
||||||
|
Writes atomically and sets secure file permissions on POSIX.
|
||||||
|
"""
|
||||||
license_file = get_license_file_path()
|
license_file = get_license_file_path()
|
||||||
try:
|
try:
|
||||||
# Create directory if it doesn't exist
|
parent_dir = os.path.dirname(license_file)
|
||||||
os.makedirs(os.path.dirname(license_file), exist_ok=True)
|
os.makedirs(parent_dir, exist_ok=True)
|
||||||
with open(license_file, 'w') as f:
|
|
||||||
f.write("1")
|
# 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
|
return True
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
@@ -73,12 +107,48 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|||||||
SOFTWARE."""
|
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):
|
class LicenseAgreementFrame(tk.Frame):
|
||||||
"""License agreement UI frame that can be embedded in the main window."""
|
"""License agreement UI frame that can be embedded in the main window."""
|
||||||
|
|
||||||
def __init__(self, parent, on_agree_callback):
|
def __init__(self, parent, on_agree_callback):
|
||||||
"""Initialize the license agreement frame.
|
"""Initialize the license agreement frame.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
parent: Parent widget
|
parent: Parent widget
|
||||||
on_agree_callback: Function to call when user agrees to license
|
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.
|
Simple entry point to launch the Android file transfer application.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
from gui.main_window import main
|
||||||
# Try relative import first (when used as module)
|
from gui.license_agreement import run_windows_first_run_if_needed
|
||||||
from .gui.main_window import main
|
|
||||||
except ImportError:
|
|
||||||
# Fall back to direct import (when run directly)
|
|
||||||
from gui.main_window import main
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
run_windows_first_run_if_needed()
|
||||||
main()
|
main()
|
||||||
|
|||||||
Reference in New Issue
Block a user