1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00
joplin/packages/app-mobile/utils/fs-driver/fs-driver-rn.web.ts
2024-08-02 14:51:49 +01:00

232 lines
6.9 KiB
TypeScript

import { resolve } from 'path';
import FsDriverBase, { ReadDirStatsOptions, RemoveOptions, Stat } from '@joplin/lib/fs-driver-base';
import tarExtract, { TarExtractOptions } from './tarExtract';
import tarCreate, { TarCreateOptions } from './tarCreate';
import { Buffer } from 'buffer';
import Logger, { LogLevel, TargetType } from '@joplin/utils/Logger';
import RemoteMessenger from '@joplin/lib/utils/ipc/RemoteMessenger';
import type { AccessMode, TransferableStat, WorkerApi } from './fs-driver-rn.web.worker';
import WorkerMessenger from '@joplin/lib/utils/ipc/WorkerMessenger';
import JoplinError from '@joplin/lib/JoplinError';
type FileHandle = {
reader: ReadableStreamDefaultReader<Uint8Array>;
buffered: Buffer;
done: boolean;
};
declare global {
interface FileSystemDirectoryHandle {
entries(): AsyncIterable<[string, FileSystemFileHandle|FileSystemDirectoryHandle]>;
keys(): AsyncIterable<string>;
}
}
const logger = new Logger();
logger.addTarget(TargetType.Console);
logger.setLevel(LogLevel.Warn);
const transferableStatToStat = (stat: TransferableStat): Stat => {
return {
mtime: new Date(stat.mtime),
birthtime: new Date(stat.birthtime),
size: stat.size,
path: stat.path,
isDirectory: () => stat.isDirectory,
};
};
interface LocalWorkerApi { }
type MessengerType = RemoteMessenger<LocalWorkerApi, WorkerApi>;
let messenger: MessengerType|null = null;
// Ensures that all instances of FsDriverWeb share the same worker. This is important
// for virtual files to be correctly handled.
const getWorkerMessenger = () => {
if (messenger) {
return messenger;
}
const worker = new Worker(
// Webpack has special syntax for creating a worker. It requires use
// of import.meta.url, which is prohibited by TypeScript in CommonJS
// modules.
//
// See also https://github.com/webpack/webpack/discussions/13655#discussioncomment-8382152
//
// TODO: Remove this after migrating to ESM.
//
// eslint-disable-next-line @typescript-eslint/ban-ts-comment -- Required for webpack build (see above)
// @ts-ignore
new URL('./fs-driver-rn.web.worker.ts', import.meta.url),
);
messenger = new WorkerMessenger('fs-worker', worker, {});
return messenger;
};
export default class FsDriverWeb extends FsDriverBase {
private messenger_: RemoteMessenger<LocalWorkerApi, WorkerApi>;
public constructor() {
super();
this.messenger_ = getWorkerMessenger();
}
public override async writeFile(
path: string,
data: string|ArrayBuffer,
encoding: BufferEncoding|'Buffer' = 'base64',
options?: FileSystemCreateWritableOptions,
) {
await this.messenger_.remoteApi.writeFile(path, data, encoding, options);
}
public override async appendFile(path: string, content: string, encoding?: BufferEncoding) {
return this.writeFile(path, content, encoding, { keepExistingData: true });
}
public override async remove(path: string, { recursive = true }: RemoveOptions = {}) {
await this.messenger_.remoteApi.remove(path, { recursive });
}
public override async unlink(path: string) {
return this.messenger_.remoteApi.unlink(path);
}
public async fileAtPath(path: string) {
try {
return await this.messenger_.remoteApi.fileAtPath(path);
} catch (error) {
if (!await this.exists(path)) {
throw new JoplinError(`fileAtPath path doesn't exist: ${error}, stack: ${error.stack}`, 'ENOENT');
}
throw error;
}
}
public async readFile(path: string, encoding: BufferEncoding|'Buffer' = 'utf-8') {
logger.debug('readFile', path);
const file = await this.fileAtPath(path);
if (encoding === 'utf-8' || encoding === 'utf8') {
return await file.text();
} else if (encoding === 'Buffer') {
return Buffer.from(await file.arrayBuffer());
} else {
const buffer = Buffer.from(await file.arrayBuffer());
return buffer.toString(encoding);
}
}
public override async open(path: string, _mode = 'r'): Promise<FileHandle> {
const file = await this.fileAtPath(path);
return {
// TODO: Extra casting required by NodeJS types conflicting with DOM types.
reader: (file.stream() as unknown as ReadableStream).getReader(),
buffered: Buffer.from([]),
done: false,
};
}
public override async readFileChunkAsBuffer(handle: FileHandle, length: number): Promise<Buffer> {
let read: Buffer = handle.buffered;
while (read.byteLength < length && !handle.done) {
const { done, value } = await handle.reader.read();
handle.done = done;
if (value) {
if (read.byteLength > 0) {
read = Buffer.concat([read, value], read.byteLength + value.byteLength);
} else {
read = Buffer.from(value);
}
}
}
const result = read.subarray(0, length);
handle.buffered = read.subarray(length, read.length);
if (result.length === 0) {
return null;
} else {
return result;
}
}
public override async readFileChunk(handle: FileHandle, length: number, encoding: BufferEncoding = 'base64') {
return (await this.readFileChunkAsBuffer(handle, length))?.toString(encoding) ?? null;
}
public override async close(handle: FileHandle) {
handle.reader?.releaseLock();
handle.reader = null;
}
public override async mkdir(path: string) {
logger.debug('mkdir', path);
await this.messenger_.remoteApi.mkdir(path);
}
public override async copy(from: string, to: string) {
await this.messenger_.remoteApi.copy(from, to);
}
public override async stat(path: string): Promise<Stat|null> {
const stat = await this.messenger_.remoteApi.stat(path);
if (!stat) return null;
return transferableStatToStat(stat);
}
public override async readDirStats(path: string, options: ReadDirStatsOptions = { recursive: false }): Promise<Stat[]> {
const stats = (await this.messenger_.remoteApi.readDirStats(path, options))?.map(transferableStatToStat);
if (!stats) {
throw new JoplinError(`Path ${path} does not exist (readDirStats)`, 'ENOENT');
}
return stats;
}
public override async exists(path: string) {
return await this.messenger_.remoteApi.exists(path);
}
public resolve(...paths: string[]): string {
return resolve(...paths);
}
public override async md5File(path: string): Promise<string> {
return await this.messenger_.remoteApi.md5File(path);
}
public override async tarExtract(options: TarExtractOptions) {
await tarExtract({
cwd: '/cache/',
...options,
});
}
public override async tarCreate(options: TarCreateOptions, filePaths: string[]) {
await tarCreate({
cwd: '/cache/',
...options,
}, filePaths);
}
public override getCacheDirectoryPath(): string {
return '/cache/';
}
public override getAppDirectoryPath(): string {
return '/app/';
}
public async createReadOnlyVirtualFile(path: string, content: File) {
return this.messenger_.remoteApi.createReadOnlyVirtualFile(path, content);
}
public async mountExternalDirectory(handle: FileSystemDirectoryHandle, id: string, mode: AccessMode) {
const externalUri = await this.messenger_.remoteApi.mountExternalDirectory(handle, id, mode);
logger.info('Mounted handle with ID', id, 'at', externalUri);
return externalUri;
}
}