fix(library): keep in-place book paths absolute so uploads stay in fs scope (#4720) (#4730)

* fix(library): keep in-place book paths absolute so uploads stay in fs scope (#4720)

resolveFilePath joined `${prefix}/${path}` unconditionally. For base 'None'
(in-place / external books, whose filePath lives outside Books/<hash>/) the
prefix is empty, so an already-absolute source path became `/C:\Users\...`
on Windows (and `//Users/...` on POSIX). The native upload guard added in
#4639 (transfer_file.rs `ensure_path_allowed`) then rejected that malformed
path as "permission denied: path not in filesystem scope", so uploading a
folder-imported book failed on Windows.

Return the path verbatim when the prefix is empty.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* style(library): use btn-contrast for the Import-from-folder confirm button (#4720)

Aligns the dialog's confirm CTA with the sibling import dialogs
(ImportFromUrlDialog, FailedImportsDialog), which already use the
theme-neutral, e-ink-correct btn-contrast instead of the colored
btn-primary.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Huang Xin
2026-06-22 21:53:59 +08:00
committed by GitHub
parent 942095bcd6
commit acf2b165f3
3 changed files with 24 additions and 2 deletions
@@ -269,6 +269,22 @@ describe('BaseAppService', () => {
const result = await service.resolveFilePath('', 'Data');
expect(result).toBe('/base/books');
});
// base 'None' carries a complete, absolute source path (in-place /
// external books point `book.filePath` outside Books/<hash>/). Its prefix
// is empty, so the path must be returned verbatim. Prepending a separator
// produced `/C:\Users\...` on Windows (and `//Users/...` on POSIX), which
// the native upload guard (transfer_file.rs `ensure_path_allowed`) rejects
// as "path not in filesystem scope" — issue #4720.
test('returns an absolute path unchanged when the prefix is empty (base None)', async () => {
vi.mocked(mockFs.getPrefix).mockResolvedValue('');
const windowsPath = "C:\\Users\\me\\Documents\\books\\Alice's Adventures.epub";
expect(await service.resolveFilePath(windowsPath, 'None')).toBe(windowsPath);
const posixPath = '/Users/me/Documents/books/Alice.epub';
expect(await service.resolveFilePath(posixPath, 'None')).toBe(posixPath);
});
});
describe('settings', () => {
@@ -434,7 +434,7 @@ const ImportFromFolderDialog: React.FC<ImportFromFolderDialogProps> = ({
</button>
<button
type='button'
className={clsx('btn btn-primary btn-sm', confirmDisabled && 'btn-disabled')}
className={clsx('btn btn-contrast btn-sm', confirmDisabled && 'btn-disabled')}
disabled={confirmDisabled}
onClick={handleConfirm}
>
+7 -1
View File
@@ -164,7 +164,13 @@ export abstract class BaseAppService implements AppService {
async resolveFilePath(path: string, base: BaseDir): Promise<string> {
const prefix = await this.fs.getPrefix(base);
return path ? `${prefix}/${path}` : prefix;
if (!path) return prefix;
// `base: 'None'` carries an already-absolute source path (in-place /
// external books point `book.filePath` outside Books/<hash>/) and its
// prefix is empty. Joining unconditionally turned `C:\Users\…` into
// `/C:\Users\…` (and `/Users/…` into `//Users/…`), which the native
// upload guard rejects as outside the fs scope — issue #4720.
return prefix ? `${prefix}/${path}` : path;
}
async readDirectory(path: string, base: BaseDir): Promise<FileItem[]> {