Skip to content

Commit

Permalink
feat(useFileSystemAccess): new function (vitest-dev#1243)
Browse files Browse the repository at this point in the history
Co-authored-by: Anthony Fu <anthonyfu117@hotmail.com>
  • Loading branch information
okxiaoliang4 and antfu committed Mar 11, 2022
1 parent 2a582d4 commit 28066cb
Show file tree
Hide file tree
Showing 4 changed files with 304 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/core/index.ts
Expand Up @@ -42,6 +42,7 @@ export * from './useEventSource'
export * from './useEyeDropper'
export * from './useFavicon'
export * from './useFetch'
export * from './useFileSystemAccess'
export * from './useFocus'
export * from './useFocusWithin'
export * from './useFps'
Expand Down
79 changes: 79 additions & 0 deletions packages/core/useFileSystemAccess/demo.vue
@@ -0,0 +1,79 @@
<script setup lang="ts">
import { stringify } from '@vueuse/docs-utils'
import type { Ref } from 'vue'
import { reactive, ref } from 'vue'
import { useFileSystemAccess } from '.'
const dataType = ref('Text') as Ref<'Text' | 'ArrayBuffer' | 'Blob'>
const res = useFileSystemAccess({
dataType,
types: [{
description: 'text',
accept: {
'text/plain': ['.txt', '.html'],
},
}],
excludeAcceptAllOption: true,
})
const content = res.data
const str = stringify(reactive({
isSupported: res.isSupported,
file: res.file,
fileName: res.fileName,
fileMIME: res.fileMIME,
fileSize: res.fileSize,
fileLastModified: res.fileLastModified,
}))
async function onSave() {
await res.save()
}
</script>

<template>
<div>
<div flex="~ gap-1" items-center>
<button @click="res.open()">
Open
</button>
<button @click="res.create()">
New file
</button>
<button :disabled="!res.file.value" @click="onSave">
Save
</button>
<button :disabled="!res.file.value" @click="res.saveAs()">
Save as
</button>

<div ml5>
<div text-xs op50>
DataType
</div>
<select v-model="dataType" class="outline-none w-30 px2 py1 text-sm" border="~ main rounded">
<option value="Text">
Text
</option>
<option value="ArrayBuffer">
ArrayBuffer
</option>
<option value="Blob">
Blob
</option>
</select>
</div>
</div>

<pre class="code-block" lang="yaml">{{ str }}</pre>

<div v-if="content">
Content
<textarea
v-if="typeof content === 'string'"
v-model="content" rows="20" cols="40" w-full
/>
<span v-else>{{ content }}</span>
</div>
</div>
</template>
15 changes: 15 additions & 0 deletions packages/core/useFileSystemAccess/index.md
@@ -0,0 +1,15 @@
---
category: Browser
---

# useFileSystemAccess

Create and read and write local files with [FileSystemAccessAPI](https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API)

## Usage

```ts
import { useFileSystemAccess } from '@vueuse/core'

const { isSupported, data, file, fileName, fileMIME, fileSize, fileLastModified, create, open, save, saveAs, updateData } = useFileSystemAccess()
```
209 changes: 209 additions & 0 deletions packages/core/useFileSystemAccess/index.ts
@@ -0,0 +1,209 @@
import type { Ref } from 'vue-demi'
import { computed, ref, unref, watch } from 'vue-demi'
import type { Awaitable, MaybeRef } from '@vueuse/shared'
import type { ConfigurableWindow } from '../_configurable'
import { defaultWindow } from '../_configurable'

/**
* window.showOpenFilePicker parameters
* @see https://developer.mozilla.org/en-US/docs/Web/API/window/showOpenFilePicker#parameters
*/
export interface FileSystemAccessShowOpenFileOptions {
multiple?: boolean
types?: Array<{
description?: string
accept: Record<string, string[]>
}>
excludeAcceptAllOption?: boolean
}

/**
* window.showSaveFilePicker parameters
* @see https://developer.mozilla.org/en-US/docs/Web/API/window/showSaveFilePicker#parameters
*/
export interface FileSystemAccessShowSaveFileOptions {
suggestedName?: string
types?: Array<{
description?: string
accept: Record<string, string[]>
}>
excludeAcceptAllOption?: boolean
}

/**
* FileHandle
* @see https://developer.mozilla.org/en-US/docs/Web/API/FileSystemFileHandle
*/
export interface FileSystemFileHandle {
getFile: () => Promise<File>
createWritable: () => FileSystemWritableFileStream
}

/**
* @see https://developer.mozilla.org/en-US/docs/Web/API/FileSystemWritableFileStream
*/
interface FileSystemWritableFileStream extends WritableStream {
/**
* @see https://developer.mozilla.org/en-US/docs/Web/API/FileSystemWritableFileStream/write
*/
write: FileSystemWritableFileStreamWrite
/**
* @see https://developer.mozilla.org/en-US/docs/Web/API/FileSystemWritableFileStream/seek
*/
seek: (position: number) => Promise<void>
/**
* @see https://developer.mozilla.org/en-US/docs/Web/API/FileSystemWritableFileStream/truncate
*/
truncate: (size: number) => Promise<void>
}

/**
* FileStream.write
* @see https://developer.mozilla.org/en-US/docs/Web/API/FileSystemWritableFileStream/write
*/
interface FileSystemWritableFileStreamWrite {
(data: string | BufferSource | Blob): Promise<void>
(options: { type: 'write'; position: number; data: string | BufferSource | Blob }): Promise<void>
(options: { type: 'seek'; position: number }): Promise<void>
(options: { type: 'truncate'; size: number }): Promise<void>
}

/**
* FileStream.write
* @see https://developer.mozilla.org/en-US/docs/Web/API/FileSystemWritableFileStream/write
*/
export type FileSystemAccessWindow = Window & {
showSaveFilePicker: (options: FileSystemAccessShowSaveFileOptions) => Promise<FileSystemFileHandle>
showOpenFilePicker: (options: FileSystemAccessShowOpenFileOptions) => Promise<FileSystemFileHandle[]>
}

export type UseFileSystemAccessCommonOptions = Pick<FileSystemAccessShowOpenFileOptions, 'types' | 'excludeAcceptAllOption'>
export type UseFileSystemAccessShowSaveFileOptions = Pick<FileSystemAccessShowSaveFileOptions, 'suggestedName'>

export type UseFileSystemAccessOptions = ConfigurableWindow & UseFileSystemAccessCommonOptions & {
/**
* file data type
*/
dataType?: MaybeRef<'Text' | 'ArrayBuffer' | 'Blob'>
}

/**
* Create and read and write local files.
* @see https://vueuse.org/useElementByPoint
* @param options
*/
export function useFileSystemAccess(options: UseFileSystemAccessOptions & { dataType: 'Text' }): UseFileSystemAccessReturn<string>
export function useFileSystemAccess(options: UseFileSystemAccessOptions & { dataType: 'ArrayBuffer' }): UseFileSystemAccessReturn<ArrayBuffer>
export function useFileSystemAccess(options: UseFileSystemAccessOptions & { dataType: 'Blob' }): UseFileSystemAccessReturn<Blob>
export function useFileSystemAccess(options: UseFileSystemAccessOptions): UseFileSystemAccessReturn<string | ArrayBuffer | Blob>
export function useFileSystemAccess(options: UseFileSystemAccessOptions = {}): UseFileSystemAccessReturn<string | ArrayBuffer | Blob> {
const {
window: _window = defaultWindow,
dataType = 'Text',
} = unref(options)
const window = _window as FileSystemAccessWindow
const isSupported = Boolean(window && 'showSaveFilePicker' in window && 'showOpenFilePicker' in window)

const fileHandle = ref<FileSystemFileHandle>()
const data = ref<string | ArrayBuffer | Blob>()

const file = ref<File>()
const fileName = computed(() => file.value?.name ?? '')
const fileMIME = computed(() => file.value?.type ?? '')
const fileSize = computed(() => file.value?.size ?? 0)
const fileLastModified = computed(() => file.value?.lastModified ?? 0)

async function open(_options: UseFileSystemAccessCommonOptions = {}) {
if (!isSupported)
return
const [handle] = await window.showOpenFilePicker({ ...unref(options), ..._options })
fileHandle.value = handle
await updateFile()
await updateData()
}

async function create(_options: UseFileSystemAccessShowSaveFileOptions = {}) {
if (!isSupported)
return
fileHandle.value = await (window as FileSystemAccessWindow).showSaveFilePicker({ ...unref(options), ..._options })
data.value = undefined
await updateFile()
await updateData()
}

async function save(_options: UseFileSystemAccessShowSaveFileOptions = {}) {
if (!isSupported)
return

if (!fileHandle.value)
// save as
return saveAs(_options)

if (data.value) {
const writableStream = await fileHandle.value.createWritable()
await writableStream.write(data.value)
await writableStream.close()
}
await updateFile()
}

async function saveAs(_options: UseFileSystemAccessShowSaveFileOptions = {}) {
if (!isSupported)
return

fileHandle.value = await (window as FileSystemAccessWindow).showSaveFilePicker({ ...unref(options), ..._options })

if (data.value) {
const writableStream = await fileHandle.value.createWritable()
await writableStream.write(data.value)
await writableStream.close()
}

await updateFile()
}

async function updateFile() {
file.value = await fileHandle.value?.getFile()
}

async function updateData() {
if (unref(dataType) === 'Text')
data.value = await file.value?.text()
if (unref(dataType) === 'ArrayBuffer')
data.value = await file.value?.arrayBuffer()
if (unref(dataType) === 'Blob')
data.value = file.value
}

watch(() => unref(dataType), updateData)

return {
isSupported,
data,
file,
fileName,
fileMIME,
fileSize,
fileLastModified,
open,
create,
save,
saveAs,
updateData,
}
}

export interface UseFileSystemAccessReturn<T = string> {
isSupported: boolean
data: Ref<T | undefined>
file: Ref<File | undefined>
fileName: Ref<string>
fileMIME: Ref<string>
fileSize: Ref<number>
fileLastModified: Ref<number>
open: (_options?: UseFileSystemAccessCommonOptions) => Awaitable<void>
create: (_options?: UseFileSystemAccessShowSaveFileOptions) => Awaitable<void>
save: (_options?: UseFileSystemAccessShowSaveFileOptions) => Awaitable<void>
saveAs: (_options?: UseFileSystemAccessShowSaveFileOptions) => Awaitable<void>
updateData: () => Awaitable<void>
}

0 comments on commit 28066cb

Please sign in to comment.