* 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:
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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[]> {
|
||||
|
||||
Reference in New Issue
Block a user