feat: add filepicker package
This commit is contained in:
112
packages/filepicker/src/Streaming/StreamingApi.ts
Normal file
112
packages/filepicker/src/Streaming/StreamingApi.ts
Normal 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'
|
||||
}
|
||||
}
|
||||
75
packages/filepicker/src/Streaming/StreamingReader.ts
Normal file
75
packages/filepicker/src/Streaming/StreamingReader.ts
Normal 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
|
||||
}
|
||||
49
packages/filepicker/src/Streaming/StreamingSaver.ts
Normal file
49
packages/filepicker/src/Streaming/StreamingSaver.ts
Normal 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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user