feat: add filepicker package

This commit is contained in:
Karol Sójko
2022-07-05 19:28:22 +02:00
parent 577da2ca84
commit d4188a3fa2
45 changed files with 5848 additions and 25 deletions

View File

@@ -0,0 +1,112 @@
import {
FileSystemApi,
DirectoryHandle,
FileHandleReadWrite,
FileHandleRead,
FileSystemNoSelection,
FileSystemResult,
} from '@standardnotes/services'
interface WebDirectoryHandle extends DirectoryHandle {
nativeHandle: FileSystemDirectoryHandle
}
interface WebFileHandleReadWrite extends FileHandleReadWrite {
nativeHandle: FileSystemFileHandle
writableStream: FileSystemWritableFileStream
}
interface WebFileHandleRead extends FileHandleRead {
nativeHandle: FileSystemFileHandle
}
export class StreamingFileApi implements FileSystemApi {
async selectDirectory(): Promise<DirectoryHandle | FileSystemNoSelection> {
try {
const nativeHandle = await window.showDirectoryPicker()
return { nativeHandle }
} catch (error) {
return 'aborted'
}
}
async createFile(directory: WebDirectoryHandle, name: string): Promise<WebFileHandleReadWrite> {
const nativeHandle = await directory.nativeHandle.getFileHandle(name, { create: true })
const writableStream = await nativeHandle.createWritable()
return {
nativeHandle,
writableStream,
}
}
async createDirectory(
parentDirectory: WebDirectoryHandle,
name: string,
): Promise<WebDirectoryHandle | FileSystemNoSelection> {
const nativeHandle = await parentDirectory.nativeHandle.getDirectoryHandle(name, { create: true })
return { nativeHandle }
}
async saveBytes(file: WebFileHandleReadWrite, bytes: Uint8Array): Promise<'success' | 'failed'> {
await file.writableStream.write(bytes)
return 'success'
}
async saveString(file: WebFileHandleReadWrite, contents: string): Promise<'success' | 'failed'> {
await file.writableStream.write(contents)
return 'success'
}
async closeFileWriteStream(file: WebFileHandleReadWrite): Promise<'success' | 'failed'> {
await file.writableStream.close()
return 'success'
}
async selectFile(): Promise<WebFileHandleRead | FileSystemNoSelection> {
try {
const selection = await window.showOpenFilePicker()
const file = selection[0]
return {
nativeHandle: file,
}
} catch (_) {
return 'aborted'
}
}
async readFile(
fileHandle: WebFileHandleRead,
onBytes: (bytes: Uint8Array, isLast: boolean) => Promise<void>,
): Promise<FileSystemResult> {
const file = await fileHandle.nativeHandle.getFile()
const stream = file.stream() as unknown as ReadableStream
const reader = stream.getReader()
let previousChunk: Uint8Array
const processChunk = async (result: ReadableStreamDefaultReadResult<Uint8Array>): Promise<void> => {
if (result.done) {
await onBytes(previousChunk, true)
return
}
if (previousChunk) {
await onBytes(previousChunk, false)
}
previousChunk = result.value
return reader.read().then(processChunk)
}
await reader.read().then(processChunk)
return 'success'
}
}

View File

@@ -0,0 +1,75 @@
import { FileReaderInterface } from './../Interface/FileReader'
import { ByteChunker } from '../Chunker/ByteChunker'
import { OnChunkCallback, FileSelectionResponse } from '../types'
interface StreamingFileReaderInterface {
getFilesFromHandles(handles: FileSystemFileHandle[]): Promise<File[]>
}
/**
* The File System Access API File Picker
* https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API
*/
export const StreamingFileReader: StreamingFileReaderInterface & FileReaderInterface = {
getFilesFromHandles,
selectFiles,
readFile,
available,
maximumFileSize,
}
function maximumFileSize(): number | undefined {
return undefined
}
function getFilesFromHandles(handles: FileSystemFileHandle[]): Promise<File[]> {
return Promise.all(handles.map((handle) => handle.getFile()))
}
async function selectFiles(): Promise<File[]> {
let selectedFilesHandles: FileSystemFileHandle[]
try {
selectedFilesHandles = await window.showOpenFilePicker({ multiple: true })
} catch (error) {
selectedFilesHandles = []
}
return getFilesFromHandles(selectedFilesHandles)
}
async function readFile(
file: File,
minimumChunkSize: number,
onChunk: OnChunkCallback,
): Promise<FileSelectionResponse> {
const byteChunker = new ByteChunker(minimumChunkSize, onChunk)
const stream = file.stream() as unknown as ReadableStream
const reader = stream.getReader()
let previousChunk: Uint8Array
const processChunk = async (result: ReadableStreamDefaultReadResult<Uint8Array>): Promise<void> => {
if (result.done) {
await byteChunker.addBytes(previousChunk, true)
return
}
if (previousChunk) {
await byteChunker.addBytes(previousChunk, false)
}
previousChunk = result.value
return reader.read().then(processChunk)
}
await reader.read().then(processChunk)
return {
name: file.name,
mimeType: file.type,
}
}
function available(): boolean {
return window.showOpenFilePicker != undefined
}

View File

@@ -0,0 +1,49 @@
/**
* The File System Access API File Picker
* https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API
*/
export class StreamingFileSaver {
public loggingEnabled = false
private writableStream!: FileSystemWritableFileStream
constructor(private name: string) {}
private log(...args: any[]): void {
if (!this.loggingEnabled) {
return
}
// eslint-disable-next-line no-console
console.log(args)
}
static available(): boolean {
return window.showSaveFilePicker != undefined
}
/** This function must be called in response to a user interaction, otherwise, it will be rejected by the browser. */
async selectFileToSaveTo(): Promise<void> {
this.log('Showing save file picker')
const downloadHandle = await window.showSaveFilePicker({
suggestedName: this.name,
})
this.writableStream = await downloadHandle.createWritable()
}
async pushBytes(bytes: Uint8Array): Promise<void> {
if (!this.writableStream) {
throw Error('Must call selectFileToSaveTo first')
}
this.log('Writing chunk to disk of size', bytes.length)
await this.writableStream.write(bytes)
}
async finish(): Promise<void> {
if (!this.writableStream) {
throw Error('Must call selectFileToSaveTo first')
}
this.log('Closing write stream')
await this.writableStream.close()
}
}