164 行
4.8 KiB
TypeScript
164 行
4.8 KiB
TypeScript
import { Platform } from 'react-native'
|
|
import { fileExtension, sanitizeFileName } from './fileName'
|
|
|
|
export {
|
|
fileExtension,
|
|
fileNameFromUrl,
|
|
parseContentDispositionFileName,
|
|
sanitizeFileName,
|
|
} from './fileName'
|
|
|
|
export type FileConflictPolicy = 'rename' | 'overwrite'
|
|
|
|
export interface FileTransferProgress {
|
|
bytesTransferred: number
|
|
totalBytes: number | null
|
|
percent: number | null
|
|
}
|
|
|
|
export interface FileDownloadTask {
|
|
cancel(): void
|
|
result: Promise<{ path: string }>
|
|
}
|
|
|
|
const MIME_BY_EXTENSION: Readonly<Record<string, string>> = {
|
|
pdf: 'application/pdf',
|
|
png: 'image/png',
|
|
jpg: 'image/jpeg',
|
|
jpeg: 'image/jpeg',
|
|
gif: 'image/gif',
|
|
webp: 'image/webp',
|
|
bmp: 'image/bmp',
|
|
svg: 'image/svg+xml',
|
|
doc: 'application/msword',
|
|
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
xls: 'application/vnd.ms-excel',
|
|
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
ppt: 'application/vnd.ms-powerpoint',
|
|
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
zip: 'application/zip',
|
|
rar: 'application/vnd.rar',
|
|
txt: 'text/plain',
|
|
csv: 'text/csv',
|
|
json: 'application/json',
|
|
mp4: 'video/mp4',
|
|
mp3: 'audio/mpeg',
|
|
}
|
|
|
|
function blobUtil(): typeof import('react-native-blob-util').default {
|
|
// Lazy loading avoids touching the native module until a file operation is requested.
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
return (
|
|
require('react-native-blob-util') as {
|
|
default: typeof import('react-native-blob-util').default
|
|
}
|
|
).default
|
|
}
|
|
|
|
export function inferMimeType(pathOrName: string): string {
|
|
return MIME_BY_EXTENSION[fileExtension(pathOrName)] ?? 'application/octet-stream'
|
|
}
|
|
|
|
export function getFileDirectories() {
|
|
return blobUtil().fs.dirs
|
|
}
|
|
|
|
export async function fileExists(path: string): Promise<boolean> {
|
|
return blobUtil().fs.exists(path)
|
|
}
|
|
|
|
export async function ensureDirectory(path: string): Promise<void> {
|
|
if (!(await fileExists(path))) await blobUtil().fs.mkdir(path)
|
|
}
|
|
|
|
export async function resolveAvailableFilePath(
|
|
directory: string,
|
|
fileName: string,
|
|
conflict: FileConflictPolicy = 'rename',
|
|
): Promise<string> {
|
|
const safeName = sanitizeFileName(fileName)
|
|
const initial = `${directory.replace(/\/$/, '')}/${safeName}`
|
|
if (conflict === 'overwrite' || !(await fileExists(initial))) return initial
|
|
|
|
const dot = safeName.lastIndexOf('.')
|
|
const base = dot > 0 ? safeName.slice(0, dot) : safeName
|
|
const extension = dot > 0 ? safeName.slice(dot) : ''
|
|
for (let index = 1; ; index += 1) {
|
|
const candidate = `${directory.replace(/\/$/, '')}/${base}(${index})${extension}`
|
|
if (!(await fileExists(candidate))) return candidate
|
|
}
|
|
}
|
|
|
|
export async function writeBase64File(path: string, base64: string): Promise<void> {
|
|
await blobUtil().fs.writeFile(path, base64, 'base64')
|
|
}
|
|
|
|
export async function readFileAsBase64(path: string): Promise<string> {
|
|
return blobUtil().fs.readFile(path, 'base64')
|
|
}
|
|
|
|
export async function deleteFile(path: string): Promise<void> {
|
|
if (await fileExists(path)) await blobUtil().fs.unlink(path)
|
|
}
|
|
|
|
export async function openLocalFile(
|
|
path: string,
|
|
mimeType = inferMimeType(path),
|
|
): Promise<boolean> {
|
|
try {
|
|
if (Platform.OS === 'android') {
|
|
const result = await blobUtil().android.actionViewIntent(path, mimeType)
|
|
return result !== false
|
|
}
|
|
await blobUtil().ios.openDocument(path)
|
|
return true
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
export function downloadFileToPath(
|
|
url: string,
|
|
path: string,
|
|
options: {
|
|
headers?: Record<string, string>
|
|
onProgress?: (progress: FileTransferProgress) => void
|
|
} = {},
|
|
): FileDownloadTask {
|
|
const task = blobUtil().config({ path }).fetch('GET', url, options.headers)
|
|
task.progress({ interval: 300 }, (received: number, total: number) => {
|
|
const bytesTransferred = received
|
|
const totalValue = total
|
|
const totalBytes = Number.isFinite(totalValue) && totalValue > 0 ? totalValue : null
|
|
options.onProgress?.({
|
|
bytesTransferred,
|
|
totalBytes,
|
|
percent:
|
|
totalBytes === null
|
|
? null
|
|
: Math.min(100, Math.round((bytesTransferred / totalBytes) * 100)),
|
|
})
|
|
})
|
|
return {
|
|
cancel: () => task.cancel(),
|
|
result: task.then(response => ({ path: response.path() })),
|
|
}
|
|
}
|
|
|
|
export async function registerAndroidDownloadedFile(options: {
|
|
path: string
|
|
title: string
|
|
description?: string
|
|
mimeType?: string
|
|
showNotification?: boolean
|
|
}): Promise<void> {
|
|
if (Platform.OS !== 'android') return
|
|
await blobUtil().android.addCompleteDownload({
|
|
path: options.path,
|
|
title: options.title,
|
|
description: options.description ?? options.title,
|
|
mime: options.mimeType ?? inferMimeType(options.path),
|
|
showNotification: options.showNotification ?? true,
|
|
})
|
|
}
|