mirror of
https://github.com/LifeArchiveProject/WeChatDataAnalysis.git
synced 2026-06-18 15:54:08 +08:00
fix(desktop-output): 移除安装目录 output 链接,避免更新/卸载误删用户数据
- 安装/卸载阶段清理旧版 output junction/symlink - 打包版写入 output-location.txt/open-output.cmd 指向真实 output 目录 - 设置页展示 output 路径并支持一键打开
This commit is contained in:
@@ -14,6 +14,34 @@
|
||||
|
||||
Var WDA_InstallDirPage
|
||||
|
||||
!macro customInit
|
||||
; Safety: older versions created an `output` junction inside the install directory that points to the
|
||||
; per-user AppData `output` folder. Some uninstall/update flows may traverse that junction and delete
|
||||
; real user data. Remove it as early as possible during install/update.
|
||||
Call WDA_RemoveLegacyOutputLink
|
||||
!macroend
|
||||
|
||||
!macro customInstall
|
||||
; Provide a safe, non-junction way for users to locate the real per-user output directory.
|
||||
; The actual data is NOT stored inside $INSTDIR (it is wiped on update/reinstall).
|
||||
; `open-output.cmd` uses %APPDATA% so it works for the current user.
|
||||
FileOpen $0 "$INSTDIR\output-location.txt" w
|
||||
FileWrite $0 "WeChatDataAnalysis output folder (per user):$\r$\n%APPDATA%\\${APP_PACKAGE_NAME}\\output$\r$\n"
|
||||
FileClose $0
|
||||
|
||||
FileOpen $1 "$INSTDIR\open-output.cmd" w
|
||||
; NSIS escaping: use $\" to output a literal quote character into the .cmd file.
|
||||
FileWrite $1 "@echo off$\r$\nexplorer $\"%APPDATA%\\${APP_PACKAGE_NAME}\\output$\"$\r$\n"
|
||||
FileClose $1
|
||||
!macroend
|
||||
|
||||
Function WDA_RemoveLegacyOutputLink
|
||||
; $INSTDIR is usually the full install directory. Be defensive and also try the nested path
|
||||
; in case the installer is running before electron-builder appends "\${APP_FILENAME}".
|
||||
RMDir "$INSTDIR\output"
|
||||
RMDir "$INSTDIR\${APP_FILENAME}\output"
|
||||
FunctionEnd
|
||||
|
||||
!macro customPageAfterChangeDir
|
||||
; Add a confirmation page after the directory picker so users clearly see
|
||||
; the final install location (includes the app sub-folder).
|
||||
@@ -90,6 +118,10 @@ Var /GLOBAL WDA_DeleteUserData
|
||||
!macro customUnInit
|
||||
; Default: keep user data (also applies to silent uninstall / update uninstall).
|
||||
StrCpy $WDA_DeleteUserData "0"
|
||||
|
||||
; Safety: if an older build created an `output` junction inside the install dir, remove it early so
|
||||
; directory cleanup can't traverse it and delete the real per-user output folder.
|
||||
RMDir "$INSTDIR\output"
|
||||
!macroend
|
||||
|
||||
!macro customUnWelcomePage
|
||||
|
||||
+75
-12
@@ -163,7 +163,11 @@ function getExeDir() {
|
||||
|
||||
function ensureOutputLink() {
|
||||
// Users often expect an `output/` folder near the installed exe. We keep the real data
|
||||
// in the per-user data dir, and (when possible) create a Windows junction next to the exe.
|
||||
// in the per-user data dir.
|
||||
//
|
||||
// NOTE: We intentionally avoid creating a junction/symlink inside the install directory.
|
||||
// Some uninstall/update flows may traverse reparse points and delete the target directory,
|
||||
// causing data loss (the install dir is removed on every update/reinstall).
|
||||
if (!app.isPackaged) return;
|
||||
|
||||
const exeDir = getExeDir();
|
||||
@@ -171,26 +175,56 @@ function ensureOutputLink() {
|
||||
if (!exeDir || !dataDir) return;
|
||||
|
||||
const target = path.join(dataDir, "output");
|
||||
const linkPath = path.join(exeDir, "output");
|
||||
const legacyLinkPath = path.join(exeDir, "output");
|
||||
|
||||
// If the target doesn't exist yet, create it so the link points somewhere real.
|
||||
// Ensure the real output dir exists.
|
||||
try {
|
||||
fs.mkdirSync(target, { recursive: true });
|
||||
} catch {}
|
||||
|
||||
// If something already exists at linkPath, do not overwrite it.
|
||||
// Best-effort: remove a legacy junction/symlink at `exeDir/output` so uninstallers can't
|
||||
// accidentally traverse it and delete the real per-user output directory.
|
||||
try {
|
||||
if (fs.existsSync(linkPath)) return;
|
||||
const st = fs.lstatSync(legacyLinkPath);
|
||||
if (st.isSymbolicLink()) {
|
||||
try {
|
||||
fs.unlinkSync(legacyLinkPath);
|
||||
logMain(`[main] removed legacy output link: ${legacyLinkPath}`);
|
||||
} catch (err) {
|
||||
logMain(`[main] failed to remove legacy output link: ${err?.message || err}`);
|
||||
}
|
||||
} else if (st.isDirectory()) {
|
||||
const entries = fs.readdirSync(legacyLinkPath);
|
||||
if (Array.isArray(entries) && entries.length === 0) {
|
||||
// Remove an empty real directory to reduce confusion (it will be recreated by the backend if needed).
|
||||
fs.rmdirSync(legacyLinkPath);
|
||||
} else {
|
||||
// Do not overwrite non-empty directories to avoid data loss.
|
||||
// Note: data stored here will be wiped on update/reinstall.
|
||||
logMain(
|
||||
`[main] output dir exists in install dir (not a link): ${legacyLinkPath}. real data dir output: ${target}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
logMain(`[main] output path exists and is not a directory/link: ${legacyLinkPath}`);
|
||||
}
|
||||
} catch {
|
||||
return;
|
||||
// Doesn't exist yet.
|
||||
}
|
||||
|
||||
// Best-effort: drop a helper file next to the exe so users can find the real data.
|
||||
// This avoids the data-loss risks of using junctions/symlinks under the install directory.
|
||||
try {
|
||||
fs.symlinkSync(target, linkPath, "junction");
|
||||
logMain(`[main] created output link: ${linkPath} -> ${target}`);
|
||||
} catch (err) {
|
||||
logMain(`[main] failed to create output link: ${err?.message || err}`);
|
||||
}
|
||||
const p = path.join(exeDir, "output-location.txt");
|
||||
const text = `WeChatDataAnalysis data directory\n\nOutput folder:\n${target}\n`;
|
||||
fs.writeFileSync(p, text, { encoding: "utf8" });
|
||||
} catch {}
|
||||
|
||||
try {
|
||||
const p = path.join(exeDir, "open-output.cmd");
|
||||
const text = `@echo off\r\nexplorer \"${target}\"\r\n`;
|
||||
fs.writeFileSync(p, text, { encoding: "utf8" });
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function getMainLogPath() {
|
||||
@@ -1254,6 +1288,30 @@ function registerWindowIpc() {
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("app:getOutputDir", () => {
|
||||
const dir = resolveDataDir();
|
||||
if (!dir) return "";
|
||||
return path.join(dir, "output");
|
||||
});
|
||||
|
||||
ipcMain.handle("app:openOutputDir", async () => {
|
||||
const dir = resolveDataDir();
|
||||
if (!dir) throw new Error("无法定位数据目录");
|
||||
const outDir = path.join(dir, "output");
|
||||
try {
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
} catch {}
|
||||
try {
|
||||
const err = await shell.openPath(outDir);
|
||||
if (err) throw new Error(err);
|
||||
return { success: true, path: outDir };
|
||||
} catch (e) {
|
||||
const message = e?.message || String(e);
|
||||
logMain(`[main] openOutputDir failed: ${message}`);
|
||||
throw new Error(message);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("app:checkForUpdates", async () => {
|
||||
return await checkForUpdatesInternal();
|
||||
});
|
||||
@@ -1272,6 +1330,11 @@ function registerWindowIpc() {
|
||||
}
|
||||
|
||||
try {
|
||||
// Safety: remove legacy `output` junctions in the install dir before triggering the NSIS update/uninstall.
|
||||
// Some uninstall flows may traverse reparse points and delete the real per-user output directory.
|
||||
try {
|
||||
ensureOutputLink();
|
||||
} catch {}
|
||||
autoUpdater.quitAndInstall(false, true);
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
@@ -1312,7 +1375,7 @@ async function main() {
|
||||
registerWindowIpc();
|
||||
registerDebugShortcuts();
|
||||
|
||||
// Resolve/create the data dir early so we can log reliably and (optionally) place a link
|
||||
// Resolve/create the data dir early so we can log reliably and place helper files
|
||||
// next to the installed exe for easier access.
|
||||
resolveDataDir();
|
||||
ensureOutputLink();
|
||||
|
||||
@@ -19,6 +19,10 @@ contextBridge.exposeInMainWorld("wechatDesktop", {
|
||||
|
||||
chooseDirectory: (options = {}) => ipcRenderer.invoke("dialog:chooseDirectory", options),
|
||||
|
||||
// Data/output folder helpers
|
||||
getOutputDir: () => ipcRenderer.invoke("app:getOutputDir"),
|
||||
openOutputDir: () => ipcRenderer.invoke("app:openOutputDir"),
|
||||
|
||||
// Auto update
|
||||
getVersion: () => ipcRenderer.invoke("app:getVersion"),
|
||||
checkForUpdates: () => ipcRenderer.invoke("app:checkForUpdates"),
|
||||
|
||||
@@ -142,6 +142,24 @@
|
||||
<div v-if="desktopBackendPortError" class="text-xs text-red-600 whitespace-pre-wrap -mt-1.5">
|
||||
{{ desktopBackendPortError }}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="text-[13px] font-medium text-[#222]">output 目录</div>
|
||||
<div class="mt-0.5 text-[11px] text-[#909090] break-words">{{ desktopOutputDirText }}</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="shrink-0 rounded-[6px] border border-[#e2e2e2] bg-white px-2 py-1 text-[12px] text-[#222] transition hover:bg-[#f9f9f9] disabled:cursor-not-allowed disabled:opacity-50"
|
||||
:disabled="!isDesktopEnv || desktopOutputDirLoading"
|
||||
@click="onDesktopOpenOutputDir"
|
||||
>
|
||||
打开 output
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="desktopOutputDirError" class="text-xs text-red-600 whitespace-pre-wrap -mt-1.5">
|
||||
{{ desktopOutputDirError }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -288,6 +306,15 @@ const desktopBackendPortApplying = ref(false)
|
||||
const desktopBackendPortError = ref('')
|
||||
const desktopBackendPortDefault = ref(10392)
|
||||
|
||||
const desktopOutputDir = ref('')
|
||||
const desktopOutputDirLoading = ref(false)
|
||||
const desktopOutputDirError = ref('')
|
||||
const desktopOutputDirText = computed(() => {
|
||||
if (!isDesktopEnv.value) return '仅桌面端可用'
|
||||
const v = String(desktopOutputDir.value || '').trim()
|
||||
return v || '—'
|
||||
})
|
||||
|
||||
const switchTrackClass = (enabled, disabled = false) => {
|
||||
if (disabled) return enabled ? 'bg-[#07b75b] opacity-50 cursor-not-allowed' : 'bg-[#d0d0d0] opacity-50 cursor-not-allowed'
|
||||
return enabled ? 'bg-[#07b75b] hover:brightness-95' : 'bg-[#d0d0d0] hover:brightness-95'
|
||||
@@ -437,6 +464,36 @@ const refreshDesktopBackendPort = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const refreshDesktopOutputDir = async () => {
|
||||
if (!process.client || typeof window === 'undefined') return
|
||||
if (!window.wechatDesktop?.getOutputDir) return
|
||||
desktopOutputDirLoading.value = true
|
||||
desktopOutputDirError.value = ''
|
||||
try {
|
||||
const v = await window.wechatDesktop.getOutputDir()
|
||||
desktopOutputDir.value = String(v || '').trim()
|
||||
} catch (e) {
|
||||
desktopOutputDirError.value = e?.message || '读取 output 目录失败'
|
||||
} finally {
|
||||
desktopOutputDirLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const onDesktopOpenOutputDir = async () => {
|
||||
if (!process.client || typeof window === 'undefined') return
|
||||
if (!window.wechatDesktop?.openOutputDir) return
|
||||
desktopOutputDirLoading.value = true
|
||||
desktopOutputDirError.value = ''
|
||||
try {
|
||||
const res = await window.wechatDesktop.openOutputDir()
|
||||
if (res?.path) desktopOutputDir.value = String(res.path || '').trim()
|
||||
} catch (e) {
|
||||
desktopOutputDirError.value = e?.message || '打开 output 目录失败'
|
||||
} finally {
|
||||
desktopOutputDirLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const applyDesktopBackendPort = async () => {
|
||||
if (!process.client || typeof window === 'undefined') return
|
||||
const raw = String(desktopBackendPortInput.value || '').trim()
|
||||
@@ -567,6 +624,7 @@ onMounted(async () => {
|
||||
void desktopUpdate.initListeners()
|
||||
await refreshDesktopAutoLaunch()
|
||||
await refreshDesktopCloseBehavior()
|
||||
await refreshDesktopOutputDir()
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
|
||||
Reference in New Issue
Block a user