forked from akai/readest
refactor: unified path resolver on all platforms (#1730)
This commit is contained in:
@@ -43,7 +43,18 @@
|
||||
"identifier": "fs:allow-cache-read",
|
||||
"allow": [
|
||||
{
|
||||
"path": "$CACHEDIR/**/*"
|
||||
"path": "$APPCACHE/**/*"
|
||||
},
|
||||
{
|
||||
"path": "/private/var/mobile/Containers/Data/Application/**/*"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"identifier": "fs:allow-cache-write",
|
||||
"allow": [
|
||||
{
|
||||
"path": "$APPCACHE/**/*"
|
||||
},
|
||||
{
|
||||
"path": "/private/var/mobile/Containers/Data/Application/**/*"
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"allow": [
|
||||
"$RESOURCE/**",
|
||||
"$APPDATA/**/*",
|
||||
"$APPCACHE/**/*",
|
||||
"$TEMP/**/*",
|
||||
"/private/var/mobile/Containers/Data/Application/**/*"
|
||||
],
|
||||
|
||||
@@ -16,7 +16,7 @@ const UpdaterPage = () => {
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className='px-12 py-4'>
|
||||
<div className='bg-base-100 px-12 py-4'>
|
||||
<UpdaterContent />
|
||||
</div>
|
||||
</Suspense>
|
||||
|
||||
@@ -143,6 +143,7 @@ const BookCover: React.FC<BookCoverProps> = memo<BookCoverProps>(
|
||||
return (
|
||||
prevProps.book.coverImageUrl === nextProps.book.coverImageUrl &&
|
||||
prevProps.book.metadata?.coverImageUrl === nextProps.book.metadata?.coverImageUrl &&
|
||||
prevProps.book.updatedAt === nextProps.book.updatedAt &&
|
||||
prevProps.mode === nextProps.mode &&
|
||||
prevProps.coverFit === nextProps.coverFit &&
|
||||
prevProps.isPreview === nextProps.isPreview &&
|
||||
|
||||
@@ -106,6 +106,7 @@ export const UpdaterContent = ({
|
||||
}
|
||||
};
|
||||
const checkAndroidUpdate = async () => {
|
||||
if (!appService) return;
|
||||
const fetch = isTauriAppPlatform() ? tauriFetch : window.fetch;
|
||||
const response = await fetch(READEST_UPDATER_FILE);
|
||||
const data = await response.json();
|
||||
@@ -114,7 +115,7 @@ export const UpdaterContent = ({
|
||||
const platformKey = OS_ARCH === 'aarch64' ? 'android-arm64' : 'android-universal';
|
||||
const arch = OS_ARCH === 'aarch64' ? 'arm64' : 'universal';
|
||||
const downloadUrl = data.platforms[platformKey]?.url as string;
|
||||
const cachePrefix = appService?.fs.getPrefix('Cache');
|
||||
const cachePrefix = await appService.fs.getPrefix('Cache');
|
||||
const apkFilePath = `${cachePrefix}/Readest_${data.version}_${arch}.apk`;
|
||||
setUpdate({
|
||||
currentVersion,
|
||||
|
||||
@@ -180,8 +180,8 @@ const BookDetailEdit: React.FC<BookDetailEditProps> = ({
|
||||
files = (await selectImageFileTauri()) as string[];
|
||||
if (appService && files.length > 0) {
|
||||
metadata.coverImageFile = files[0]!;
|
||||
const tempName = 'cover-temp.png';
|
||||
const cachePrefix = appService.fs.getPrefix('Cache');
|
||||
const tempName = `cover-${Date.now()}.png`;
|
||||
const cachePrefix = await appService.fs.getPrefix('Cache');
|
||||
await appService.fs.copyFile(files[0]!, tempName, 'Cache');
|
||||
metadata.coverImageUrl = await appService.fs.getURL(`${cachePrefix}/${tempName}`);
|
||||
setNewCoverImageUrl(metadata.coverImageUrl!);
|
||||
@@ -224,7 +224,7 @@ const BookDetailEdit: React.FC<BookDetailEditProps> = ({
|
||||
'border border-gray-300 bg-white hover:bg-gray-50',
|
||||
'disabled:cursor-not-allowed disabled:opacity-50',
|
||||
'text-sm sm:text-xs',
|
||||
isCoverLocked && '!text-base-content !bg-base-200',
|
||||
isCoverLocked ? '!text-base-content !bg-base-200' : '!text-gray-500',
|
||||
)}
|
||||
title={_('Change cover image')}
|
||||
>
|
||||
|
||||
@@ -46,6 +46,13 @@ import { ProgressHandler } from '@/utils/transfer';
|
||||
import { TxtToEpubConverter } from '@/utils/txt';
|
||||
import { BOOK_FILE_NOT_FOUND_ERROR } from './errors';
|
||||
|
||||
export type ResolvedPath = {
|
||||
baseDir: number;
|
||||
basePrefix: () => Promise<string>;
|
||||
fp: string;
|
||||
base: BaseDir;
|
||||
};
|
||||
|
||||
export abstract class BaseAppService implements AppService {
|
||||
osPlatform: OsPlatform = getOSPlatform();
|
||||
appPlatform: AppPlatform = 'tauri';
|
||||
@@ -70,11 +77,9 @@ export abstract class BaseAppService implements AppService {
|
||||
|
||||
abstract fs: FileSystem;
|
||||
|
||||
abstract resolvePath(fp: string, base: BaseDir): { baseDir: number; base: BaseDir; fp: string };
|
||||
abstract resolvePath(fp: string, base: BaseDir): ResolvedPath;
|
||||
abstract getCoverImageUrl(book: Book): string;
|
||||
abstract getCoverImageBlobUrl(book: Book): Promise<string>;
|
||||
abstract getInitBooksDir(): Promise<string>;
|
||||
abstract getCacheDir(): Promise<string>;
|
||||
abstract selectDirectory(): Promise<string>;
|
||||
abstract selectFiles(name: string, extensions: string[]): Promise<string[]>;
|
||||
|
||||
@@ -102,7 +107,7 @@ export abstract class BaseAppService implements AppService {
|
||||
settings = JSON.parse(txt as string);
|
||||
const version = settings.version ?? 0;
|
||||
if (this.isAppDataSandbox || version < SYSTEM_SETTINGS_VERSION) {
|
||||
settings.localBooksDir = await this.getInitBooksDir();
|
||||
settings.localBooksDir = await this.fs.getPrefix('Books');
|
||||
settings.version = SYSTEM_SETTINGS_VERSION;
|
||||
}
|
||||
settings = { ...DEFAULT_SYSTEM_SETTINGS, ...settings };
|
||||
@@ -115,7 +120,7 @@ export abstract class BaseAppService implements AppService {
|
||||
settings = {
|
||||
...DEFAULT_SYSTEM_SETTINGS,
|
||||
version: SYSTEM_SETTINGS_VERSION,
|
||||
localBooksDir: await this.getInitBooksDir(),
|
||||
localBooksDir: await this.fs.getPrefix('Books'),
|
||||
globalReadSettings: {
|
||||
...DEFAULT_READSETTINGS,
|
||||
...(this.isMobile ? DEFAULT_MOBILE_READSETTINGS : {}),
|
||||
@@ -129,15 +134,6 @@ export abstract class BaseAppService implements AppService {
|
||||
}
|
||||
|
||||
this.localBooksDir = settings.localBooksDir;
|
||||
const cacheDir = await this.getCacheDir();
|
||||
this.fs.getPrefix = (baseDir: BaseDir) => {
|
||||
if (baseDir === 'Books') {
|
||||
return this.localBooksDir;
|
||||
} else if (baseDir === 'Cache') {
|
||||
return cacheDir;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
return settings;
|
||||
}
|
||||
|
||||
@@ -414,12 +410,15 @@ export abstract class BaseAppService implements AppService {
|
||||
const lfp = getCoverFilename(book);
|
||||
const cfp = `${CLOUD_BOOKS_SUBDIR}/${lfp}`;
|
||||
await this.downloadCloudFile(lfp, cfp, handleProgress);
|
||||
completedFiles.count++;
|
||||
bookCoverDownloaded = true;
|
||||
}
|
||||
} catch (error) {
|
||||
// don't throw error here since some books may not have cover images at all
|
||||
console.log(`Failed to download cover file for book: '${book.title}'`, error);
|
||||
} finally {
|
||||
if (needDownCover) {
|
||||
completedFiles.count++;
|
||||
}
|
||||
}
|
||||
|
||||
if (needDownBook) {
|
||||
|
||||
@@ -14,17 +14,25 @@ import {
|
||||
} from '@tauri-apps/plugin-fs';
|
||||
import { convertFileSrc } from '@tauri-apps/api/core';
|
||||
import { open as openDialog } from '@tauri-apps/plugin-dialog';
|
||||
import { join, basename, appDataDir, appCacheDir } from '@tauri-apps/api/path';
|
||||
import {
|
||||
join,
|
||||
basename,
|
||||
appDataDir,
|
||||
appConfigDir,
|
||||
appCacheDir,
|
||||
appLogDir,
|
||||
tempDir,
|
||||
} from '@tauri-apps/api/path';
|
||||
import { type as osType } from '@tauri-apps/plugin-os';
|
||||
|
||||
import { Book } from '@/types/book';
|
||||
import { FileSystem, BaseDir, AppPlatform } from '@/types/system';
|
||||
import { getOSPlatform, isContentURI, isValidURL } from '@/utils/misc';
|
||||
import { getOSPlatform, isContentURI, isFileURI, isValidURL } from '@/utils/misc';
|
||||
import { getCoverFilename, getFilename } from '@/utils/book';
|
||||
import { copyURIToPath } from '@/utils/bridge';
|
||||
import { NativeFile, RemoteFile } from '@/utils/file';
|
||||
|
||||
import { BaseAppService } from './appService';
|
||||
import { BaseAppService, ResolvedPath } from './appService';
|
||||
import { LOCAL_BOOKS_SUBDIR } from './constants';
|
||||
|
||||
declare global {
|
||||
@@ -36,38 +44,49 @@ declare global {
|
||||
|
||||
const OS_TYPE = osType();
|
||||
|
||||
const resolvePath = (fp: string, base: BaseDir): { baseDir: number; base: BaseDir; fp: string } => {
|
||||
// Usage:
|
||||
// 1. baseDir + fp
|
||||
// 2. basePrefix + fp or the getPrefix util in FileSystem
|
||||
const resolvePath = (path: string, base: BaseDir): ResolvedPath => {
|
||||
switch (base) {
|
||||
case 'Settings':
|
||||
return { baseDir: BaseDirectory.AppConfig, fp, base };
|
||||
return { baseDir: BaseDirectory.AppConfig, basePrefix: appConfigDir, fp: path, base };
|
||||
case 'Data':
|
||||
return { baseDir: BaseDirectory.AppData, fp, base };
|
||||
return { baseDir: BaseDirectory.AppData, basePrefix: appDataDir, fp: path, base };
|
||||
case 'Cache':
|
||||
return { baseDir: BaseDirectory.AppCache, fp, base };
|
||||
return { baseDir: BaseDirectory.AppCache, basePrefix: appCacheDir, fp: path, base };
|
||||
case 'Log':
|
||||
return { baseDir: BaseDirectory.AppLog, fp, base };
|
||||
return { baseDir: BaseDirectory.AppLog, basePrefix: appLogDir, fp: path, base };
|
||||
case 'Books':
|
||||
return {
|
||||
baseDir: BaseDirectory.AppData,
|
||||
fp: `${LOCAL_BOOKS_SUBDIR}/${fp}`,
|
||||
basePrefix: appDataDir,
|
||||
fp: `${LOCAL_BOOKS_SUBDIR}/${path}`,
|
||||
base,
|
||||
};
|
||||
case 'None':
|
||||
return {
|
||||
baseDir: 0,
|
||||
fp,
|
||||
basePrefix: async () => '',
|
||||
fp: path,
|
||||
base,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
baseDir: BaseDirectory.Temp,
|
||||
fp,
|
||||
basePrefix: tempDir,
|
||||
fp: path,
|
||||
base,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const nativeFileSystem: FileSystem = {
|
||||
async getPrefix(base: BaseDir) {
|
||||
const { basePrefix, fp } = resolvePath('', base);
|
||||
const basePath = await basePrefix();
|
||||
return fp ? await join(basePath, fp) : basePath;
|
||||
},
|
||||
getURL(path: string) {
|
||||
return isValidURL(path) ? path : convertFileSrc(path);
|
||||
},
|
||||
@@ -89,8 +108,8 @@ export const nativeFileSystem: FileSystem = {
|
||||
} else {
|
||||
// Otherwise, for content:// URIs (e.g. from MediaStore, Drive, or third-party apps),
|
||||
// we cannot access the file directly — so we copy it to a temporary cache location.
|
||||
const prefix = this.getPrefix('Cache');
|
||||
const dst = `${prefix}/${fname}`;
|
||||
const prefix = await this.getPrefix('Cache');
|
||||
const dst = await join(prefix, fname);
|
||||
const res = await copyURIToPath({ uri: path, dst });
|
||||
if (!res.success) {
|
||||
console.error('Failed to open file:', res);
|
||||
@@ -99,8 +118,11 @@ export const nativeFileSystem: FileSystem = {
|
||||
return await new NativeFile(dst, fname, base ? baseDir : null).open();
|
||||
}
|
||||
} else {
|
||||
const prefix = this.getPrefix(base);
|
||||
const absolutePath = path.startsWith('/') ? path : prefix ? await join(prefix, path) : null;
|
||||
const prefix = await this.getPrefix(base);
|
||||
if (isFileURI(path)) {
|
||||
path = path.replace(/^file:\/\//, '');
|
||||
}
|
||||
const absolutePath = path.startsWith('/') ? path : await join(prefix, path);
|
||||
if (absolutePath && OS_TYPE !== 'android') {
|
||||
// NOTE: RemoteFile currently performs about 2× faster than NativeFile
|
||||
// due to an unresolved performance issue in Tauri (see tauri-apps/tauri#9190).
|
||||
@@ -114,13 +136,13 @@ export const nativeFileSystem: FileSystem = {
|
||||
},
|
||||
async copyFile(srcPath: string, dstPath: string, base: BaseDir) {
|
||||
if (isContentURI(srcPath)) {
|
||||
const prefix = this.getPrefix(base);
|
||||
const prefix = await this.getPrefix(base);
|
||||
if (!prefix) {
|
||||
throw new Error('Invalid base directory');
|
||||
}
|
||||
const res = await copyURIToPath({
|
||||
uri: srcPath,
|
||||
dst: `${prefix}/${dstPath}`,
|
||||
dst: await join(prefix, dstPath),
|
||||
});
|
||||
if (!res.success) {
|
||||
console.error('Failed to copy file:', res);
|
||||
@@ -201,9 +223,6 @@ export const nativeFileSystem: FileSystem = {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
getPrefix() {
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
export class NativeAppService extends BaseAppService {
|
||||
@@ -232,18 +251,10 @@ export class NativeAppService extends BaseAppService {
|
||||
(OS_TYPE === 'ios' && getOSPlatform() === 'ios') || OS_TYPE === 'android';
|
||||
override distChannel = process.env['NEXT_PUBLIC_DIST_CHANNEL'] || 'readest';
|
||||
|
||||
override resolvePath(fp: string, base: BaseDir): { baseDir: number; base: BaseDir; fp: string } {
|
||||
override resolvePath(fp: string, base: BaseDir): ResolvedPath {
|
||||
return resolvePath(fp, base);
|
||||
}
|
||||
|
||||
async getInitBooksDir(): Promise<string> {
|
||||
return join(await appDataDir(), LOCAL_BOOKS_SUBDIR);
|
||||
}
|
||||
|
||||
async getCacheDir(): Promise<string> {
|
||||
return await appCacheDir();
|
||||
}
|
||||
|
||||
async selectDirectory(): Promise<string> {
|
||||
const selected = await openDialog({
|
||||
directory: true,
|
||||
|
||||
@@ -3,19 +3,20 @@ import { FileSystem, BaseDir, AppPlatform } from '@/types/system';
|
||||
import { getCoverFilename } from '@/utils/book';
|
||||
import { getOSPlatform, isValidURL } from '@/utils/misc';
|
||||
import { RemoteFile } from '@/utils/file';
|
||||
|
||||
import { isPWA } from './environment';
|
||||
import { BaseAppService } from './appService';
|
||||
import { BaseAppService, ResolvedPath } from './appService';
|
||||
import { LOCAL_BOOKS_SUBDIR } from './constants';
|
||||
|
||||
const resolvePath = (fp: string, base: BaseDir): { baseDir: number; base: BaseDir; fp: string } => {
|
||||
const basePrefix = async () => '';
|
||||
|
||||
const resolvePath = (path: string, base: BaseDir): ResolvedPath => {
|
||||
switch (base) {
|
||||
case 'Books':
|
||||
return { baseDir: 0, fp: `${LOCAL_BOOKS_SUBDIR}/${fp}`, base };
|
||||
return { baseDir: 0, basePrefix, fp: `${LOCAL_BOOKS_SUBDIR}/${path}`, base };
|
||||
case 'None':
|
||||
return { baseDir: 0, fp, base };
|
||||
return { baseDir: 0, basePrefix, fp: path, base };
|
||||
default:
|
||||
return { baseDir: 0, fp: `${base}/${fp}`, base };
|
||||
return { baseDir: 0, basePrefix, fp: `${base}/${path}`, base };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -188,8 +189,10 @@ const indexedDBFileSystem: FileSystem = {
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
},
|
||||
getPrefix() {
|
||||
return null;
|
||||
async getPrefix(base: BaseDir) {
|
||||
const { basePrefix, fp } = resolvePath('', base);
|
||||
const basePath = await basePrefix();
|
||||
return fp ? `${basePath}/${fp}` : basePath;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -199,18 +202,10 @@ export class WebAppService extends BaseAppService {
|
||||
override appPlatform = 'web' as AppPlatform;
|
||||
override hasSafeAreaInset = isPWA();
|
||||
|
||||
override resolvePath(fp: string, base: BaseDir): { baseDir: number; base: BaseDir; fp: string } {
|
||||
override resolvePath(fp: string, base: BaseDir): ResolvedPath {
|
||||
return resolvePath(fp, base);
|
||||
}
|
||||
|
||||
async getInitBooksDir(): Promise<string> {
|
||||
return LOCAL_BOOKS_SUBDIR;
|
||||
}
|
||||
|
||||
async getCacheDir(): Promise<string> {
|
||||
return 'Cache';
|
||||
}
|
||||
|
||||
async selectDirectory(): Promise<string> {
|
||||
throw new Error('selectDirectory is not supported in browser');
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ export interface FileSystem {
|
||||
createDir(path: string, base: BaseDir, recursive?: boolean): Promise<void>;
|
||||
removeDir(path: string, base: BaseDir, recursive?: boolean): Promise<void>;
|
||||
exists(path: string, base: BaseDir): Promise<boolean>;
|
||||
getPrefix(base: BaseDir): string | null;
|
||||
getPrefix(base: BaseDir): Promise<string>;
|
||||
}
|
||||
|
||||
export interface AppService {
|
||||
|
||||
Reference in New Issue
Block a user