Repository navigation
Expand file tree
/
Copy pathinstall.sh
More file actions
executable file
·419 lines (361 loc) · 15.7 KB
/
Copy pathinstall.sh
File metadata and controls
executable file
·419 lines (361 loc) · 15.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
#!/usr/bin/env bash
set -euo pipefail
readonly UPSTREAM_ARCHIVE_URL="https://github.com/Yizhou147/Droidspaces-USB-Manager/archive/refs/heads/main.tar.gz"
readonly INSTALL_DIR="/usr/share/usb-manager"
readonly SUDOERS_FILE="/etc/sudoers.d/droidspaces-usb-manager"
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" 2>/dev/null && pwd -P || true)"
WORK_DIR=""
SOURCE_DIR=""
TARGET_USER=""
TARGET_GROUP=""
TARGET_HOME=""
PACKAGE_FAMILY=""
UI_LANG="en"
detect_language() {
local locale_name="${LC_ALL:-${LC_MESSAGES:-${LANG:-C}}}"
locale_name="${locale_name,,}"
if [[ "$locale_name" == zh* ]]; then
UI_LANG="zh"
fi
}
msg() {
if [[ "$UI_LANG" == "zh" ]]; then
printf '%s' "$1"
else
printf '%s' "$2"
fi
}
log() {
printf '[usb-manager] %s\n' "$(msg "$1" "$2")"
}
die() {
printf '[usb-manager] %s: %s\n' "$(msg '错误' 'Error')" "$(msg "$1" "$2")" >&2
exit 1
}
usage() {
cat <<EOF
$(msg '用法' 'Usage'): $0 [--user USER] [--source DIR]
--user USER $(msg '为指定用户配置免密码 USB 管理权限。' 'Configure passwordless USB management permissions for USER.')
--source DIR $(msg '使用本地 Droidspaces-USB-Manager 源码目录。' 'Use a local Droidspaces-USB-Manager source directory.')
-h, --help $(msg '显示此帮助。' 'Show this help.')
EOF
}
cleanup() {
if [[ -n "$WORK_DIR" && -d "$WORK_DIR" ]]; then
rm -rf -- "$WORK_DIR"
fi
}
trap cleanup EXIT
require_root() {
if (( EUID == 0 )); then
return
fi
if command -v sudo >/dev/null 2>&1; then
log "正在通过 sudo 重新运行安装程序..." "Restarting the installer with sudo..."
exec sudo --preserve-env=LANG,LC_ALL,LC_MESSAGES,USB_MANAGER_SOURCE_DIR -- "$0" "$@"
fi
die "请使用 root 账户运行此脚本。" "Please run this script as root."
}
parse_args() {
while (($# > 0)); do
case "$1" in
--user)
(($# >= 2)) || die "--user 缺少用户名。" "--user requires a username."
TARGET_USER="$2"
shift 2
;;
--source)
(($# >= 2)) || die "--source 缺少目录。" "--source requires a directory."
SOURCE_DIR="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
die "未知参数: $1" "Unknown argument: $1"
;;
esac
done
}
detect_target() {
[[ -r /etc/os-release ]] || die "无法读取 /etc/os-release。" "Unable to read /etc/os-release."
# shellcheck disable=SC1091
source /etc/os-release
[[ -n "${ID:-}" ]] || die "/etc/os-release 缺少 ID。" "/etc/os-release does not contain ID."
case "$ID" in
debian|ubuntu) PACKAGE_FAMILY="apt" ;;
fedora) PACKAGE_FAMILY="dnf" ;;
arch|archarm) PACKAGE_FAMILY="pacman" ;;
*)
case " ${ID_LIKE:-} " in
*" debian "*|*" ubuntu "*) PACKAGE_FAMILY="apt" ;;
*" fedora "*|*" rhel "*) PACKAGE_FAMILY="dnf" ;;
*" arch "*) PACKAGE_FAMILY="pacman" ;;
*)
die "不支持当前系统 ${PRETTY_NAME:-$ID}。支持 Debian/Ubuntu、Fedora 和 Arch 系发行版。" \
"Unsupported system: ${PRETTY_NAME:-$ID}. Debian/Ubuntu, Fedora, and Arch families are supported."
;;
esac
;;
esac
log "已识别系统: ${PRETTY_NAME:-$ID} (${PACKAGE_FAMILY})" \
"Detected system: ${PRETTY_NAME:-$ID} (${PACKAGE_FAMILY})"
}
resolve_target_user() {
local candidate=""
local passwd_entry=""
if [[ -z "$TARGET_USER" && -n "${SUDO_USER:-}" && "${SUDO_USER}" != "root" ]]; then
TARGET_USER="$SUDO_USER"
fi
if [[ -z "$TARGET_USER" ]]; then
candidate="$(logname 2>/dev/null || true)"
if [[ -n "$candidate" && "$candidate" != "root" ]]; then
TARGET_USER="$candidate"
fi
fi
if [[ -z "$TARGET_USER" ]]; then
TARGET_USER="$(awk -F: '$3 >= 1000 && $3 < 65534 && $7 !~ /(nologin|false)$/ { print $1; exit }' /etc/passwd)"
fi
[[ -n "$TARGET_USER" ]] || die \
"无法确定桌面用户,请使用 --user USER 指定。" \
"Unable to determine the desktop user; specify it with --user USER."
[[ "$TARGET_USER" =~ ^[A-Za-z_][A-Za-z0-9_.-]*\$?$ ]] || die \
"用户名格式无效: $TARGET_USER" "Invalid username: $TARGET_USER"
passwd_entry="$(getent passwd "$TARGET_USER")" || die \
"用户不存在: $TARGET_USER" "User does not exist: $TARGET_USER"
TARGET_GROUP="$(id -gn "$TARGET_USER")"
TARGET_HOME="$(cut -d: -f6 <<<"$passwd_entry")"
[[ -n "$TARGET_HOME" && -d "$TARGET_HOME" ]] || die \
"用户主目录不存在: ${TARGET_HOME:-未知}" \
"User home directory does not exist: ${TARGET_HOME:-unknown}"
log "将为用户 ${TARGET_USER} 配置 USB 管理权限。" \
"USB management permissions will be configured for ${TARGET_USER}."
}
install_dependencies() {
log "正在安装图形界面、USB、ADB 和文件系统依赖..." \
"Installing GUI, USB, ADB, and filesystem dependencies..."
case "$PACKAGE_FAMILY" in
apt)
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y --no-install-recommends \
python3 python3-pyqt5 qtwayland5 libqt5svg5 udev util-linux sudo \
adb ntfs-3g exfatprogs desktop-file-utils gvfs-backends gvfs-fuse kio-extras \
xdg-utils ca-certificates curl wget tar
;;
dnf)
dnf install -y --setopt=install_weak_deps=False \
python3 python3-qt5 qt5-qtwayland qt5-qtsvg systemd-udev util-linux sudo \
android-tools ntfs-3g exfatprogs desktop-file-utils gvfs-mtp gvfs-fuse kio-extras \
xdg-utils ca-certificates curl wget tar
;;
pacman)
pacman -Syu --noconfirm --needed \
python python-pyqt5 qt5-wayland qt5-svg systemd util-linux sudo \
android-tools ntfs-3g exfatprogs desktop-file-utils gvfs gvfs-mtp kio-extras \
xdg-utils ca-certificates curl wget tar
;;
esac
}
has_sources() {
local base="$1"
[[ -f "$base/src/usb-manager.py" \
&& -f "$base/src/usb-passthrough.sh" \
&& -f "$base/src/usb-storage-passthrough.sh" ]]
}
download_sources() {
local archive extract_root
WORK_DIR="$(mktemp -d -t droidspaces-usb-manager.XXXXXXXX)"
archive="$WORK_DIR/upstream.tar.gz"
log "本地未找到源码,正在下载 Droidspaces-USB-Manager..." \
"Local sources were not found; downloading Droidspaces-USB-Manager..."
if command -v curl >/dev/null 2>&1; then
curl -fL --retry 3 --connect-timeout 20 "$UPSTREAM_ARCHIVE_URL" -o "$archive"
elif command -v wget >/dev/null 2>&1; then
wget -O "$archive" "$UPSTREAM_ARCHIVE_URL"
else
die "未找到 curl 或 wget,无法下载源码。" "Neither curl nor wget was found; sources cannot be downloaded."
fi
tar -xzf "$archive" -C "$WORK_DIR"
extract_root="$(find "$WORK_DIR" -mindepth 1 -maxdepth 1 -type d -print -quit)"
[[ -n "$extract_root" ]] || die "下载的源码快照内容异常。" "The downloaded source snapshot is invalid."
has_sources "$extract_root" || die "源码快照缺少必需文件。" "The source snapshot is missing required files."
SOURCE_DIR="$extract_root"
}
locate_sources() {
local candidate
local -a candidates=()
if [[ -n "${USB_MANAGER_SOURCE_DIR:-}" ]]; then
candidates+=("$USB_MANAGER_SOURCE_DIR")
fi
if [[ -n "$SOURCE_DIR" ]]; then
candidates+=("$SOURCE_DIR")
fi
if [[ -n "$SCRIPT_DIR" ]]; then
candidates+=(
"$SCRIPT_DIR/Droidspaces-USB-Manager"
"$SCRIPT_DIR/usb-manager"
"$SCRIPT_DIR/../Droidspaces-USB-Manager"
)
fi
candidates+=("$PWD")
for candidate in "${candidates[@]}"; do
if has_sources "$candidate"; then
SOURCE_DIR="$(cd -- "$candidate" && pwd -P)"
log "使用本地源码: ${SOURCE_DIR}" "Using local sources: ${SOURCE_DIR}"
return
fi
done
download_sources
}
require_commands() {
local command_name
for command_name in python3 sudo bash mount umount mknod chmod mkdir blkid xdg-open visudo desktop-file-validate; do
command -v "$command_name" >/dev/null 2>&1 || die \
"安装依赖后仍未找到命令: $command_name" \
"Command is still unavailable after dependency installation: $command_name"
done
}
install_program() {
log "正在安装 Droidspaces USB Manager..." "Installing Droidspaces USB Manager..."
install -d -m 0755 "$INSTALL_DIR" /usr/share/applications /usr/share/doc/droidspaces-usb-manager
install -m 0644 "$SOURCE_DIR/src/usb-manager.py" "$INSTALL_DIR/usb-manager.py"
install -m 0755 "$SOURCE_DIR/src/usb-passthrough.sh" "$INSTALL_DIR/usb-passthrough.sh"
install -m 0755 "$SOURCE_DIR/src/usb-storage-passthrough.sh" "$INSTALL_DIR/usb-storage-passthrough.sh"
# Install bundled icons
if [[ -d "$SOURCE_DIR/icons" ]]; then
install -d -m 0755 "$INSTALL_DIR/icons"
install -m 0644 "$SOURCE_DIR/icons/"*.svg "$INSTALL_DIR/icons/"
if [[ -f "$SOURCE_DIR/icons/LICENSE" ]]; then
install -m 0644 "$SOURCE_DIR/icons/LICENSE" "$INSTALL_DIR/icons/LICENSE"
fi
fi
# Upstream currently assumes Debian's /usr/sbin path. /usr/bin is shared by
# Debian/Ubuntu, Fedora, and Arch (including merged-/usr installations).
# Keep upstream's X11 (xcb) backend preference to avoid Wayland compatibility issues.
# Note: blkid path is resolved dynamically at runtime (BLKID_CMD), no patch needed.
sed -i \
-e 's|"dolphin", path|"xdg-open", path|g' \
-e 's|请运行: sudo apt install python3-pyqt5|请安装当前发行版的 PyQt5 软件包|g' \
"$INSTALL_DIR/usb-manager.py"
sed -i \
's|SUDOERS_FILE="/etc/sudoers.d/usb-storage"|SUDOERS_FILE="/etc/sudoers.d/droidspaces-usb-manager"|g' \
"$INSTALL_DIR/usb-storage-passthrough.sh"
if [[ -f "$SOURCE_DIR/LICENSE" ]]; then
install -m 0644 "$SOURCE_DIR/LICENSE" /usr/share/doc/droidspaces-usb-manager/LICENSE
fi
printf '%s\n' "$UPSTREAM_ARCHIVE_URL" > /usr/share/doc/droidspaces-usb-manager/upstream-source
cat > /usr/bin/usb-manager <<'EOF'
#!/usr/bin/env bash
exec python3 /usr/share/usb-manager/usb-manager.py "$@"
EOF
chmod 0755 /usr/bin/usb-manager
cat > /usr/bin/usb-passthrough <<'EOF'
#!/usr/bin/env bash
exec sudo /usr/bin/bash /usr/share/usb-manager/usb-passthrough.sh "$@"
EOF
chmod 0755 /usr/bin/usb-passthrough
cat > /usr/bin/usb-storage-passthrough <<'EOF'
#!/usr/bin/env bash
exec /usr/bin/bash /usr/share/usb-manager/usb-storage-passthrough.sh "$@"
EOF
chmod 0755 /usr/bin/usb-storage-passthrough
# Use bundled icon if available, otherwise fallback to system theme
local icon_path="${INSTALL_DIR}/icons/usb-manager.svg"
if [[ ! -f "$icon_path" ]]; then
icon_path="drive-removable-media-usb"
log "未找到打包图标,将使用系统主题图标。" "Bundled icons not found, falling back to system theme."
fi
cat > /usr/share/applications/usb-manager.desktop <<EOF
[Desktop Entry]
Name=USB Manager
Name[zh_CN]=USB 管理器
Comment=Droidspaces USB Device Manager
Comment[zh_CN]=Droidspaces USB 设备管理器
Exec=/usr/bin/usb-manager
Icon=${icon_path}
Terminal=false
Type=Application
Categories=System;
StartupNotify=false
EOF
install -d -m 0755 -o "$TARGET_USER" -g "$TARGET_GROUP" "$TARGET_HOME/Desktop"
# 若桌面快捷方式已是指向源文件的软链接,先删除再复制,避免 install 报 same file
if [ -L "$TARGET_HOME/Desktop/usb-manager.desktop" ]; then
rm -f -- "$TARGET_HOME/Desktop/usb-manager.desktop"
fi
install -m 0755 -o "$TARGET_USER" -g "$TARGET_GROUP" \
/usr/share/applications/usb-manager.desktop \
"$TARGET_HOME/Desktop/usb-manager.desktop"
}
validate_program() {
grep -Fq '"xdg-open", path' "$INSTALL_DIR/usb-manager.py" || die \
"未能应用文件管理器兼容补丁。" "The file-manager compatibility patch could not be applied."
grep -Fq "SUDOERS_FILE=\"$SUDOERS_FILE\"" "$INSTALL_DIR/usb-storage-passthrough.sh" || die \
"未能应用存储脚本权限补丁。" "The storage-script permission patch could not be applied."
python3 -c 'import pathlib, sys; p = pathlib.Path(sys.argv[1]); compile(p.read_text(encoding="utf-8"), str(p), "exec")' \
"$INSTALL_DIR/usb-manager.py"
bash -n "$INSTALL_DIR/usb-passthrough.sh"
bash -n "$INSTALL_DIR/usb-storage-passthrough.sh"
desktop-file-validate /usr/share/applications/usb-manager.desktop
desktop-file-validate "$TARGET_HOME/Desktop/usb-manager.desktop"
}
configure_permissions() {
local sudoers_tmp
sudoers_tmp="$(mktemp -t droidspaces-usb-manager-sudoers.XXXXXXXX)"
# 让桌面用户加入 ntfsusb 组(gid 1023)。
# 新版 ntfs-3g (2026.x) 挂载的 NTFS 卷固定属主为 root:1023 770,
# 普通用户加入该组后可直接读写挂载点,无需走 pkexec 认证。
if command -v groupadd >/dev/null 2>&1; then
groupadd -g 1023 ntfsusb 2>/dev/null || true
usermod -a -G 1023 "${TARGET_USER}" 2>/dev/null || true
log "已将用户 ${TARGET_USER} 加入 ntfsusb 组。" \
"User ${TARGET_USER} has been added to the ntfsusb group."
fi
cat > "$sudoers_tmp" <<EOF
# Droidspaces USB Manager: device-node creation and removable-media mounting
# blkid 同时授权 /usr/bin 与 /usr/sbin 两种布局(不同发行版/版本位置不同)
Cmnd_Alias DROIDSPACES_USB_MANAGER = /usr/bin/mount *, /usr/bin/umount *, /usr/bin/mknod *, /usr/bin/chmod *, /usr/bin/mkdir -p /dev/bus/usb/*, /usr/bin/blkid *, /usr/sbin/blkid *, /usr/bin/bash ${INSTALL_DIR}/usb-passthrough.sh
${TARGET_USER} ALL=(root) NOPASSWD: DROIDSPACES_USB_MANAGER
EOF
visudo -cf "$sudoers_tmp" >/dev/null || die \
"生成的 sudoers 配置校验失败。" "The generated sudoers configuration failed validation."
install -m 0440 "$sudoers_tmp" "$SUDOERS_FILE"
rm -f -- "$sudoers_tmp"
if [[ -f /etc/fuse.conf ]] && ! grep -Eq '^[[:space:]]*user_allow_other([[:space:]]|$)' /etc/fuse.conf; then
if grep -Eq '^[[:space:]]*#[[:space:]]*user_allow_other([[:space:]]|$)' /etc/fuse.conf; then
sed -i 's/^[[:space:]]*#[[:space:]]*user_allow_other[[:space:]]*$/user_allow_other/' /etc/fuse.conf
else
printf '\nuser_allow_other\n' >> /etc/fuse.conf
fi
fi
install -d -m 0755 /mnt/usb-storage
}
refresh_desktop_database() {
if command -v update-desktop-database >/dev/null 2>&1; then
update-desktop-database /usr/share/applications >/dev/null 2>&1 || true
fi
}
main() {
detect_language
parse_args "$@"
require_root "$@"
detect_target
resolve_target_user
install_dependencies
require_commands
locate_sources
install_program
validate_program
configure_permissions
refresh_desktop_database
log "安装完成。可从应用菜单或桌面快捷方式启动 USB Manager,也可运行 usb-manager。" \
"Installation complete. Start USB Manager from the application menu or desktop shortcut, or run usb-manager."
log "已加入 ntfsusb 组,请注销重新登录(或重启容器)后生效。" \
"Added to the ntfsusb group; log out and back in (or restart the container) for it to take effect."
log "导入 Droidspaces 容器时必须开启硬件访问。" \
"Hardware access must be enabled when importing the Droidspaces container."
}
main "$@"