Skip to content

Commit f218028

Browse files
feat: 不適切なフォルダにファイルを保存できないようにする (#1844)
* feat: 不適切なフォルダにファイルを保存できないようにする * fix: 警告の文面を変更 * fix: コメント修正 Co-authored-by: Hiroshiba <[email protected]> * fix: コメント追加 Co-authored-by: Hiroshiba <[email protected]> * refactor: 簡素化 * fix: 三項演算子で可能 * fix: ダイアログの文面を変更 * fix: for (;;)をwhile (true)へ * style: no-constant-condition checkLoops 無効化 * Update .eslintrc.js 意図を追加 Co-authored-by: Hiroshiba <[email protected]> * fix: isPathUnsafeをリネーム * fix: unsafeSaveDirsをisUnsafePathの中へ * fix: filePathが未定義の場合エラーにする * fix: より直感的に * fix: メッセージのアプリ名をapp.getName()から取得するように * fix: ダイアログ関数の戻り値を変更 * コメントを追加 --------- Co-authored-by: Hiroshiba <[email protected]>
1 parent d0fcc10 commit f218028

File tree

10 files changed

+130
-22
lines changed

10 files changed

+130
-22
lines changed

.eslintrc.js

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ module.exports = {
2626
? ["error", "unix"]
2727
: "off",
2828
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
29+
"no-constant-condition": ["error", { checkLoops: false }], // while(true) などを許可
2930
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
3031
"prettier/prettier": [
3132
"error",

src/backend/browser/sandbox.ts

+3
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@ export const api: Sandbox = {
101101
}
102102
});
103103
},
104+
showSaveDirectoryDialog(obj: { title: string }) {
105+
return showOpenDirectoryDialogImpl(obj);
106+
},
104107
showVvppOpenDialog(obj: { title: string; defaultPath?: string }) {
105108
// NOTE: 今後接続先を変える手段としてVvppが使われるかもしれないので、そのタイミングで実装する
106109
throw new Error(

src/backend/electron/main.ts

+114-18
Original file line numberDiff line numberDiff line change
@@ -618,26 +618,116 @@ ipcMainHandle("GET_ALT_PORT_INFOS", () => {
618618
return engineManager.altPortInfo;
619619
});
620620

621+
/**
622+
* 保存に適した場所を選択するかキャンセルするまでダイアログを繰り返し表示する。
623+
* アンインストール等で消えうる場所などを避ける。
624+
* @param showDialogFunction ダイアログを表示する関数
625+
*/
626+
const retryShowSaveDialogWhileSafeDir = async <
627+
T extends Electron.OpenDialogReturnValue | Electron.SaveDialogReturnValue
628+
>(
629+
showDialogFunction: () => Promise<T>
630+
): Promise<T> => {
631+
/**
632+
* 指定されたパスが安全でないかどうかを判断する
633+
*/
634+
const isUnsafePath = (filePath: string) => {
635+
const unsafeSaveDirs = [appDirPath, app.getPath("userData")]; // アンインストールで消えうるフォルダ
636+
return unsafeSaveDirs.some((unsafeDir) => {
637+
const relativePath = path.relative(unsafeDir, filePath);
638+
return !(
639+
path.isAbsolute(relativePath) ||
640+
relativePath.startsWith(`..${path.sep}`) ||
641+
relativePath === ".."
642+
);
643+
});
644+
};
645+
646+
/**
647+
* 警告ダイアログを表示し、ユーザーが再試行を選択したかどうかを返す
648+
*/
649+
const showWarningDialog = async () => {
650+
const productName = app.getName().toUpperCase();
651+
const warningResult = await dialog.showMessageBox(win, {
652+
message: `指定された保存先は${productName}により自動的に削除される可能性があります。\n他の場所に保存することをおすすめします。`,
653+
type: "warning",
654+
buttons: ["保存場所を変更", "無視して保存"],
655+
defaultId: 0,
656+
title: "警告",
657+
cancelId: 0,
658+
});
659+
return warningResult.response === 0 ? "retry" : "forceSave";
660+
};
661+
662+
while (true) {
663+
const result = await showDialogFunction();
664+
// キャンセルされた場合、結果を直ちに返す
665+
if (result.canceled) return result;
666+
667+
// 選択されたファイルパスを取得
668+
const filePath =
669+
"filePaths" in result ? result.filePaths[0] : result.filePath;
670+
671+
// filePathが未定義の場合、エラーを返す
672+
if (filePath == undefined) {
673+
throw new Error(
674+
`canseld == ${result.canceled} but filePath == ${filePath}`
675+
);
676+
}
677+
678+
// 選択されたパスが安全かどうかを確認
679+
if (isUnsafePath(filePath)) {
680+
const result = await showWarningDialog();
681+
if (result === "retry") continue; // ユーザーが保存場所を変更を選択した場合
682+
}
683+
return result; // 安全なパスが選択された場合
684+
}
685+
};
686+
621687
ipcMainHandle("SHOW_AUDIO_SAVE_DIALOG", async (_, { title, defaultPath }) => {
622-
const result = await dialog.showSaveDialog(win, {
623-
title,
624-
defaultPath,
625-
filters: [{ name: "Wave File", extensions: ["wav"] }],
626-
properties: ["createDirectory"],
627-
});
688+
const result = await retryShowSaveDialogWhileSafeDir(() =>
689+
dialog.showSaveDialog(win, {
690+
title,
691+
defaultPath,
692+
filters: [{ name: "Wave File", extensions: ["wav"] }],
693+
properties: ["createDirectory"],
694+
})
695+
);
628696
return result.filePath;
629697
});
630698

631699
ipcMainHandle("SHOW_TEXT_SAVE_DIALOG", async (_, { title, defaultPath }) => {
632-
const result = await dialog.showSaveDialog(win, {
633-
title,
634-
defaultPath,
635-
filters: [{ name: "Text File", extensions: ["txt"] }],
636-
properties: ["createDirectory"],
637-
});
700+
const result = await retryShowSaveDialogWhileSafeDir(() =>
701+
dialog.showSaveDialog(win, {
702+
title,
703+
defaultPath,
704+
filters: [{ name: "Text File", extensions: ["txt"] }],
705+
properties: ["createDirectory"],
706+
})
707+
);
638708
return result.filePath;
639709
});
640710

711+
/**
712+
* 保存先になるディレクトリを選ぶダイアログを表示する。
713+
*/
714+
ipcMainHandle("SHOW_SAVE_DIRECTORY_DIALOG", async (_, { title }) => {
715+
const result = await retryShowSaveDialogWhileSafeDir(() =>
716+
dialog.showOpenDialog(win, {
717+
title,
718+
properties: [
719+
"openDirectory",
720+
"createDirectory",
721+
"treatPackageAsDirectory",
722+
],
723+
})
724+
);
725+
if (result.canceled) {
726+
return undefined;
727+
}
728+
return result.filePaths[0];
729+
});
730+
641731
ipcMainHandle("SHOW_VVPP_OPEN_DIALOG", async (_, { title, defaultPath }) => {
642732
const result = await dialog.showOpenDialog(win, {
643733
title,
@@ -650,6 +740,10 @@ ipcMainHandle("SHOW_VVPP_OPEN_DIALOG", async (_, { title, defaultPath }) => {
650740
return result.filePaths[0];
651741
});
652742

743+
/**
744+
* ディレクトリ選択ダイアログを表示する。
745+
* 保存先として選ぶ場合は SHOW_SAVE_DIRECTORY_DIALOG を使うべき。
746+
*/
653747
ipcMainHandle("SHOW_OPEN_DIRECTORY_DIALOG", async (_, { title }) => {
654748
const result = await dialog.showOpenDialog(win, {
655749
title,
@@ -662,12 +756,14 @@ ipcMainHandle("SHOW_OPEN_DIRECTORY_DIALOG", async (_, { title }) => {
662756
});
663757

664758
ipcMainHandle("SHOW_PROJECT_SAVE_DIALOG", async (_, { title, defaultPath }) => {
665-
const result = await dialog.showSaveDialog(win, {
666-
title,
667-
defaultPath,
668-
filters: [{ name: "VOICEVOX Project file", extensions: ["vvproj"] }],
669-
properties: ["showOverwriteConfirmation"],
670-
});
759+
const result = await retryShowSaveDialogWhileSafeDir(() =>
760+
dialog.showSaveDialog(win, {
761+
title,
762+
defaultPath,
763+
filters: [{ name: "VOICEVOX Project file", extensions: ["vvproj"] }],
764+
properties: ["showOverwriteConfirmation"],
765+
})
766+
);
671767
if (result.canceled) {
672768
return undefined;
673769
}

src/backend/electron/preload.ts

+4
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ const api: Sandbox = {
7676
return ipcRendererInvoke("SHOW_TEXT_SAVE_DIALOG", { title, defaultPath });
7777
},
7878

79+
showSaveDirectoryDialog: ({ title }) => {
80+
return ipcRendererInvoke("SHOW_SAVE_DIRECTORY_DIALOG", { title });
81+
},
82+
7983
showVvppOpenDialog: ({ title, defaultPath }) => {
8084
return ipcRendererInvoke("SHOW_VVPP_OPEN_DIALOG", { title, defaultPath });
8185
},

src/components/Dialog/SettingDialog.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -1290,7 +1290,7 @@ const outputSamplingRate = computed({
12901290
});
12911291
12921292
const openFileExplore = async () => {
1293-
const path = await window.backend.showOpenDirectoryDialog({
1293+
const path = await window.backend.showSaveDirectoryDialog({
12941294
title: "書き出し先のフォルダを選択",
12951295
});
12961296
if (path) {

src/store/audio.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1395,7 +1395,7 @@ export const audioStore = createPartialStore<AudioStoreTypes>({
13951395
if (state.savingSetting.fixedExportEnabled) {
13961396
dirPath = state.savingSetting.fixedExportDir;
13971397
} else {
1398-
dirPath ??= await window.backend.showOpenDirectoryDialog({
1398+
dirPath ??= await window.backend.showSaveDirectoryDialog({
13991399
title: "音声を保存",
14001400
});
14011401
}

src/type/ipc.ts

+5
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@ export type IpcIHData = {
7979
return?: string;
8080
};
8181

82+
SHOW_SAVE_DIRECTORY_DIALOG: {
83+
args: [obj: { title: string }];
84+
return?: string;
85+
};
86+
8287
SHOW_VVPP_OPEN_DIALOG: {
8388
args: [obj: { title: string; defaultPath?: string }];
8489
return?: string;

src/type/preload.ts

+1
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ export interface Sandbox {
182182
title: string;
183183
defaultPath?: string;
184184
}): Promise<string | undefined>;
185+
showSaveDirectoryDialog(obj: { title: string }): Promise<string | undefined>;
185186
showVvppOpenDialog(obj: {
186187
title: string;
187188
defaultPath?: string;

tests/e2e/browser/スクリーンショット.spec.ts

-1
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,6 @@ test("メイン画面の表示", async ({ page }) => {
125125
test.skip(process.platform !== "win32", "Windows以外のためスキップします");
126126
await navigateToMain(page);
127127

128-
// eslint-disable-next-line no-constant-condition
129128
while (true) {
130129
await page.locator(".audio-cell:nth-child(1) .q-field").click();
131130
await page.waitForTimeout(100);

tests/e2e/electron/example.spec.ts

-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ test.beforeAll(async () => {
88
dotenv.config(); // FIXME: エンジンの設定直読み
99

1010
console.log("Waiting for main.js to be built...");
11-
// eslint-disable-next-line no-constant-condition
1211
while (true) {
1312
try {
1413
await fs.access("./dist/main.js");

0 commit comments

Comments
 (0)