124 lines
3.9 KiB
Python
124 lines
3.9 KiB
Python
"""将用户选中的 .lnk / .url 解析为实际要存储的路径(目标程序或 URL)。"""
|
||
import base64
|
||
import json
|
||
import os
|
||
import re
|
||
import subprocess
|
||
import sys
|
||
|
||
|
||
def _looks_like_shell_lnk(path: str) -> bool:
|
||
"""Windows Shell Link 文件头为 4C 00 00 00(扩展名异常时仍可识别)。"""
|
||
try:
|
||
with open(path, "rb") as f:
|
||
return f.read(4) == b"L\x00\x00\x00"
|
||
except OSError:
|
||
return False
|
||
|
||
|
||
def _read_internet_shortcut_url(path: str) -> str | None:
|
||
try:
|
||
with open(path, "r", encoding="utf-8", errors="ignore") as f:
|
||
for line in f:
|
||
s = line.strip()
|
||
if s[:4].upper() == "URL=":
|
||
return s[4:].strip()
|
||
except OSError:
|
||
pass
|
||
return None
|
||
|
||
|
||
def _resolve_windows_lnk(lnk_path: str) -> str | None:
|
||
if sys.platform != "win32":
|
||
return None
|
||
env = os.environ.copy()
|
||
env["LNK_B64"] = base64.b64encode(os.path.normpath(lnk_path).encode("utf-8")).decode(
|
||
"ascii"
|
||
)
|
||
ps = (
|
||
"$p=[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($env:LNK_B64));"
|
||
"$sc=(New-Object -ComObject WScript.Shell).CreateShortcut($p);"
|
||
"@{ t=[string]$sc.TargetPath; a=[string]$sc.Arguments }|ConvertTo-Json -Compress"
|
||
)
|
||
try:
|
||
r = subprocess.run(
|
||
[
|
||
"powershell",
|
||
"-NoProfile",
|
||
"-Sta",
|
||
"-Command",
|
||
ps,
|
||
],
|
||
capture_output=True,
|
||
text=True,
|
||
encoding="utf-8",
|
||
errors="replace",
|
||
env=env,
|
||
timeout=15,
|
||
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
||
)
|
||
raw = (r.stdout or "").strip().lstrip("\ufeff")
|
||
if not raw:
|
||
return None
|
||
data = json.loads(raw)
|
||
target = (data.get("t") or "").strip()
|
||
args = (data.get("a") or "").strip()
|
||
if not target:
|
||
return None
|
||
low = target.lower()
|
||
if low.endswith("cmd.exe") or low.endswith("\\cmd.exe"):
|
||
for m in re.finditer(
|
||
r"([a-zA-Z]:[^\"'\s]+\.(?:bat|cmd))|\x22([a-zA-Z]:[^\"]+\.(?:bat|cmd))\x22",
|
||
args,
|
||
re.IGNORECASE,
|
||
):
|
||
cand = (m.group(1) or m.group(2) or "").strip('"')
|
||
if cand and os.path.isfile(cand):
|
||
return os.path.normpath(cand)
|
||
if os.path.isfile(target) or low.startswith("\\\\"):
|
||
return os.path.normpath(target)
|
||
return os.path.normpath(target)
|
||
except Exception:
|
||
return None
|
||
|
||
|
||
def path_for_storage(local_path: str) -> str:
|
||
"""
|
||
拖拽/选择文件时写入数据库的 path:.lnk/.url 解析为目标,其它保持原路径。
|
||
解析失败时退回本地路径。
|
||
"""
|
||
if not local_path:
|
||
return local_path
|
||
local_path = os.path.normpath(local_path.strip())
|
||
|
||
ext = os.path.splitext(local_path)[1].lower()
|
||
if ext == ".url":
|
||
try:
|
||
if os.path.isfile(local_path):
|
||
url = _read_internet_shortcut_url(local_path)
|
||
if url:
|
||
return url
|
||
except (OSError, ValueError):
|
||
pass
|
||
return local_path
|
||
|
||
try:
|
||
is_file = os.path.isfile(local_path)
|
||
except (OSError, ValueError):
|
||
is_file = False
|
||
|
||
is_lnk = ext in (".lnk", ".ink") or (
|
||
is_file and _looks_like_shell_lnk(local_path)
|
||
)
|
||
if is_lnk and is_file:
|
||
resolved = _resolve_windows_lnk(local_path)
|
||
if resolved:
|
||
return resolved
|
||
return local_path
|
||
|
||
|
||
def item_name_from_sources(local_path: str, store_path: str) -> str:
|
||
"""列表显示名:仍用快捷方式/所选文件的文件名(无扩展名),与原来行为一致。"""
|
||
base = os.path.splitext(os.path.basename(local_path))[0]
|
||
return base or os.path.basename(store_path)
|