migrate file system api
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import {copyAsync} from 'expo-file-system/legacy'
|
||||
import uuid from 'react-native-uuid'
|
||||
import {File, Paths} from 'expo-file-system'
|
||||
import {type BlobRef, type Client, type EncodingString} from '@atproto/lex'
|
||||
|
||||
import {safeDeleteAsync} from '#/lib/media/manip'
|
||||
import {safeDelete} from '#/lib/media/manip'
|
||||
|
||||
/**
|
||||
* The blob-upload response body: `{blob}`. lex `Client.uploadBlob` returns the
|
||||
@@ -89,9 +90,9 @@ async function withSafeFile<T>(
|
||||
// Since we don't "own" the file, we should avoid renaming or modifying it.
|
||||
// Instead, let's copy it to a temporary file and use that (then remove the
|
||||
// temporary file).
|
||||
const newPath = uri.replace(/\.jpe?g$/, '.bin')
|
||||
const newPath = new File(Paths.cache, `${uuid.v4()}.bin`).uri
|
||||
try {
|
||||
await copyAsync({from: uri, to: newPath})
|
||||
await new File(uri).copy(new File(newPath))
|
||||
} catch {
|
||||
// Failed to copy the file, just use the original
|
||||
return await fn(uri)
|
||||
@@ -100,7 +101,7 @@ async function withSafeFile<T>(
|
||||
return await fn(newPath)
|
||||
} finally {
|
||||
// Remove the temporary file
|
||||
await safeDeleteAsync(newPath)
|
||||
safeDelete(newPath)
|
||||
}
|
||||
} else {
|
||||
return fn(uri)
|
||||
|
||||
+71
-113
@@ -1,17 +1,6 @@
|
||||
import {Image as RNImage} from 'react-native'
|
||||
import uuid from 'react-native-uuid'
|
||||
import {
|
||||
cacheDirectory,
|
||||
copyAsync,
|
||||
createDownloadResumable,
|
||||
deleteAsync,
|
||||
EncodingType,
|
||||
getInfoAsync,
|
||||
makeDirectoryAsync,
|
||||
moveAsync,
|
||||
StorageAccessFramework,
|
||||
writeAsStringAsync,
|
||||
} from 'expo-file-system/legacy'
|
||||
import {Directory, EncodingType, File, Paths} from 'expo-file-system'
|
||||
import {SaveFormat} from 'expo-image-manipulator'
|
||||
import * as MediaLibrary from 'expo-media-library/legacy'
|
||||
import * as Sharing from 'expo-sharing'
|
||||
@@ -68,7 +57,7 @@ export async function downloadAndResize(opts: DownloadAndResizeOpts) {
|
||||
maxSize: opts.maxSize,
|
||||
})
|
||||
} finally {
|
||||
void safeDeleteAsync(path)
|
||||
safeDelete(path)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,9 +83,9 @@ export async function shareImageModal({uri}: {uri: string}) {
|
||||
UTI: 'image/jpeg',
|
||||
})
|
||||
} finally {
|
||||
await safeDeleteAsync(downloadedPath)
|
||||
if (jpegUri) await safeDeleteAsync(jpegUri)
|
||||
if (imagePath) await safeDeleteAsync(imagePath)
|
||||
safeDelete(downloadedPath)
|
||||
if (jpegUri) safeDelete(jpegUri)
|
||||
if (imagePath) safeDelete(imagePath)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,7 +165,7 @@ export async function saveImageToMediaLibrary({uri}: {uri: string}) {
|
||||
})
|
||||
throw err
|
||||
} finally {
|
||||
safeDeleteAsync(imagePath)
|
||||
safeDelete(imagePath)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,19 +228,19 @@ async function doResize(
|
||||
|
||||
intermediateUris.push(resizeRes.uri)
|
||||
|
||||
const fileInfo = await getInfoAsync(resizeRes.uri)
|
||||
if (!fileInfo.exists) {
|
||||
const file = new File(resizeRes.uri)
|
||||
if (!file.exists) {
|
||||
throw new Error(
|
||||
'The image manipulation library failed to create a new image.',
|
||||
)
|
||||
}
|
||||
|
||||
if (fileInfo.size < opts.maxSize) {
|
||||
if (file.size < opts.maxSize) {
|
||||
minQualityPercentage = qualityPercentage
|
||||
newDataUri = {
|
||||
path: normalizePath(resizeRes.uri),
|
||||
mime: 'image/jpeg',
|
||||
size: fileInfo.size,
|
||||
size: file.size,
|
||||
width: resizeRes.width,
|
||||
height: resizeRes.height,
|
||||
}
|
||||
@@ -271,12 +260,12 @@ async function doResize(
|
||||
newDataUri = undefined
|
||||
throw err
|
||||
} finally {
|
||||
await safeDeleteAsync(imageRes.uri)
|
||||
await Promise.all(
|
||||
intermediateUris
|
||||
.filter(uri => newDataUri?.path !== normalizePath(uri))
|
||||
.map(safeDeleteAsync),
|
||||
)
|
||||
safeDelete(imageRes.uri)
|
||||
for (const intermediateUri of intermediateUris) {
|
||||
if (newDataUri?.path !== normalizePath(intermediateUri)) {
|
||||
safeDelete(intermediateUri)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,44 +277,29 @@ async function moveToPermanentPath(path: string, ext: string): Promise<string> {
|
||||
*/
|
||||
const filename = uuid.v4()
|
||||
|
||||
// cacheDirectory will not ever be null on native, but it could be on web. This function only ever gets called on
|
||||
// native so we assert as a string.
|
||||
const destinationPath = joinPath(cacheDirectory as string, filename + ext)
|
||||
await copyAsync({
|
||||
from: normalizePath(path),
|
||||
to: normalizePath(destinationPath),
|
||||
})
|
||||
safeDeleteAsync(path)
|
||||
return normalizePath(destinationPath)
|
||||
const destination = new File(Paths.cache, filename + ext)
|
||||
await new File(normalizePath(path)).copy(destination)
|
||||
safeDelete(path)
|
||||
return normalizePath(destination.uri)
|
||||
}
|
||||
|
||||
export async function safeDeleteAsync(path: string) {
|
||||
// Normalize is necessary for Android, otherwise it doesn't delete.
|
||||
export function safeDelete(path: string) {
|
||||
const normalizedPath = normalizePath(path)
|
||||
try {
|
||||
await deleteAsync(normalizedPath, {idempotent: true})
|
||||
const info = Paths.info(normalizedPath)
|
||||
if (info.isDirectory) {
|
||||
new Directory(normalizedPath).delete()
|
||||
} else if (info.exists) {
|
||||
new File(normalizedPath).delete()
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to delete file', e)
|
||||
}
|
||||
}
|
||||
|
||||
function joinPath(a: string, b: string) {
|
||||
if (a.endsWith('/')) {
|
||||
if (b.startsWith('/')) {
|
||||
return a.slice(0, -1) + b
|
||||
}
|
||||
return a + b
|
||||
} else if (b.startsWith('/')) {
|
||||
return a + b
|
||||
}
|
||||
return a + '/' + b
|
||||
}
|
||||
|
||||
function normalizePath(str: string, allPlatforms = false): string {
|
||||
if (IS_ANDROID || allPlatforms) {
|
||||
if (!str.startsWith('file://')) {
|
||||
return `file://${str}`
|
||||
}
|
||||
function normalizePath(str: string): string {
|
||||
if (str.startsWith('/')) {
|
||||
return `file://${str}`
|
||||
}
|
||||
return str
|
||||
}
|
||||
@@ -356,20 +330,9 @@ export async function saveToDevice(
|
||||
})
|
||||
return true
|
||||
} else {
|
||||
const permissions =
|
||||
await StorageAccessFramework.requestDirectoryPermissionsAsync()
|
||||
|
||||
if (!permissions.granted) {
|
||||
return false
|
||||
}
|
||||
|
||||
const fileUrl = await StorageAccessFramework.createFileAsync(
|
||||
permissions.directoryUri,
|
||||
filename,
|
||||
type,
|
||||
)
|
||||
|
||||
await writeAsStringAsync(fileUrl, encoded, {
|
||||
const directory = await Directory.pickDirectoryAsync()
|
||||
const file = directory.createFile(filename, type)
|
||||
file.write(encoded, {
|
||||
encoding: EncodingType.Base64,
|
||||
})
|
||||
return true
|
||||
@@ -385,65 +348,60 @@ async function withTempFile<T>(
|
||||
encoded: string,
|
||||
cb: (url: string) => T | Promise<T>,
|
||||
): Promise<T> {
|
||||
// cacheDirectory will not ever be null so we assert as a string.
|
||||
// Using a directory so that the file name is not a random string
|
||||
const tmpDirUri = joinPath(cacheDirectory as string, String(uuid.v4()))
|
||||
await makeDirectoryAsync(tmpDirUri, {intermediates: true})
|
||||
const tmpDir = new Directory(Paths.cache, String(uuid.v4()))
|
||||
tmpDir.create({intermediates: true})
|
||||
|
||||
try {
|
||||
const tmpFileUrl = joinPath(tmpDirUri, filename)
|
||||
await writeAsStringAsync(tmpFileUrl, encoded, {
|
||||
const tmpFile = new File(tmpDir, filename)
|
||||
tmpFile.write(encoded, {
|
||||
encoding: EncodingType.Base64,
|
||||
})
|
||||
|
||||
return await cb(tmpFileUrl)
|
||||
return await cb(tmpFile.uri)
|
||||
} finally {
|
||||
safeDeleteAsync(tmpDirUri)
|
||||
safeDelete(tmpDir.uri)
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadImage(uri: string, destName: string, timeout: number) {
|
||||
// Download to a temp path first, then rename with the correct extension
|
||||
// based on the response's mimeType.
|
||||
const tempPath = `${cacheDirectory ?? ''}/${destName}.bin`
|
||||
const dlResumable = createDownloadResumable(uri, tempPath, {cache: true})
|
||||
/*
|
||||
* Download into a temporary directory so Expo can derive a filename from
|
||||
* the response headers. We then use that file's MIME type to choose the
|
||||
* permanent extension.
|
||||
*/
|
||||
const tempDir = new Directory(Paths.cache, `${destName}-download`)
|
||||
tempDir.create({intermediates: true})
|
||||
|
||||
const controller = new AbortController()
|
||||
let timedOut = false
|
||||
let downloadedPath: string | undefined
|
||||
let finalPath: string | undefined
|
||||
const to1 = setTimeout(() => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
timedOut = true
|
||||
void dlResumable.cancelAsync().catch(() => undefined)
|
||||
controller.abort()
|
||||
}, timeout)
|
||||
|
||||
try {
|
||||
let dlRes
|
||||
try {
|
||||
dlRes = await dlResumable.downloadAsync()
|
||||
} finally {
|
||||
clearTimeout(to1)
|
||||
}
|
||||
|
||||
if (!dlRes?.uri) {
|
||||
if (timedOut) {
|
||||
throw new Error('Failed to download image - timed out')
|
||||
} else {
|
||||
throw new Error('Failed to download image - dlRes is undefined')
|
||||
}
|
||||
}
|
||||
|
||||
downloadedPath = dlRes.uri
|
||||
const ext = extFromMime(dlRes.mimeType)
|
||||
finalPath = `${cacheDirectory ?? ''}/${destName}.${ext}`
|
||||
await moveAsync({from: downloadedPath, to: finalPath})
|
||||
|
||||
return normalizePath(finalPath)
|
||||
} catch (err) {
|
||||
await Promise.all(
|
||||
[...new Set([tempPath, downloadedPath, finalPath])]
|
||||
.filter(path => path !== undefined)
|
||||
.map(safeDeleteAsync),
|
||||
const downloaded = await File.downloadFileAsync(uri, tempDir, {
|
||||
idempotent: true,
|
||||
signal: controller.signal,
|
||||
})
|
||||
const ext = extFromMime(downloaded.type)
|
||||
const destination = new File(
|
||||
Paths.cache,
|
||||
ext ? `${destName}.${ext}` : `${destName}.bin`,
|
||||
)
|
||||
await downloaded.move(destination)
|
||||
|
||||
return normalizePath(destination.uri)
|
||||
} catch (err) {
|
||||
if (timedOut) {
|
||||
throw new Error('Failed to download image - timed out')
|
||||
}
|
||||
|
||||
throw err
|
||||
} finally {
|
||||
clearTimeout(timeoutId)
|
||||
safeDelete(tempDir.uri)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -454,6 +412,6 @@ const MIME_TO_EXT: Record<string, string> = {
|
||||
'image/gif': 'gif',
|
||||
}
|
||||
|
||||
function extFromMime(mimeType?: string | null): string {
|
||||
return (mimeType && MIME_TO_EXT[mimeType]) || 'jpg'
|
||||
function extFromMime(mimeType?: string | null): string | undefined {
|
||||
return mimeType ? MIME_TO_EXT[mimeType] : undefined
|
||||
}
|
||||
|
||||
@@ -193,6 +193,6 @@ function downloadUrl(href: string, filename: string) {
|
||||
document.body.removeChild(a)
|
||||
}
|
||||
|
||||
export async function safeDeleteAsync() {
|
||||
export function safeDelete() {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import {Asset} from 'expo-asset'
|
||||
import {
|
||||
documentDirectory,
|
||||
getInfoAsync,
|
||||
readDirectoryAsync,
|
||||
} from 'expo-file-system/legacy'
|
||||
import {Directory, File, Paths} from 'expo-file-system'
|
||||
import {type ImagePickerResult} from 'expo-image-picker'
|
||||
import ExpoImageCropTool, {
|
||||
type OpenCropperOptions,
|
||||
@@ -19,27 +15,29 @@ async function getFile() {
|
||||
return await getAndroidFile()
|
||||
}
|
||||
|
||||
const imagesDir = documentDirectory!
|
||||
const imagesDir = Paths.document.uri
|
||||
.replace(/\/?$/, '/')
|
||||
.split('/')
|
||||
.slice(0, -6)
|
||||
.concat(['Media', 'DCIM', '100APPLE'])
|
||||
.join('/')
|
||||
|
||||
let files = await readDirectoryAsync(imagesDir)
|
||||
files = files.filter(file => file.endsWith('.JPG'))
|
||||
const file = `${imagesDir}/${files[0]}`
|
||||
const file = new Directory(imagesDir)
|
||||
.list()
|
||||
.find(
|
||||
(entry): entry is File =>
|
||||
entry instanceof File && entry.name.endsWith('.JPG'),
|
||||
)
|
||||
|
||||
const fileInfo = await getInfoAsync(file)
|
||||
|
||||
if (!fileInfo.exists) {
|
||||
if (!file?.exists) {
|
||||
throw new Error('Failed to get file info')
|
||||
}
|
||||
|
||||
return await compressIfNeeded(
|
||||
{
|
||||
path: file,
|
||||
path: file.uri,
|
||||
mime: 'image/jpeg',
|
||||
size: fileInfo.size,
|
||||
size: file.size,
|
||||
width: 4288,
|
||||
height: 2848,
|
||||
},
|
||||
@@ -61,10 +59,9 @@ async function getAndroidFile() {
|
||||
)
|
||||
await asset.downloadAsync()
|
||||
|
||||
const path = asset.localUri!
|
||||
const fileInfo = await getInfoAsync(path)
|
||||
const file = new File(asset.localUri!)
|
||||
|
||||
if (!fileInfo.exists) {
|
||||
if (!file.exists) {
|
||||
throw new Error('Failed to get file info')
|
||||
}
|
||||
|
||||
@@ -75,9 +72,9 @@ async function getAndroidFile() {
|
||||
*/
|
||||
return await compressIfNeeded(
|
||||
{
|
||||
path,
|
||||
path: file.uri,
|
||||
mime: 'image/jpeg',
|
||||
size: fileInfo.size,
|
||||
size: file.size,
|
||||
width: 1432,
|
||||
height: 1025,
|
||||
},
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {getInfoAsync} from 'expo-file-system/legacy'
|
||||
import {File} from 'expo-file-system'
|
||||
|
||||
export async function getUriSize(uri: string): Promise<number> {
|
||||
const info = await getInfoAsync(uri)
|
||||
if (!info.exists) {
|
||||
export function getUriSize(uri: string): Promise<number> {
|
||||
const file = new File(uri)
|
||||
if (!file.exists) {
|
||||
throw new Error('Failed to read image size')
|
||||
}
|
||||
return info.size
|
||||
return Promise.resolve(file.size)
|
||||
}
|
||||
|
||||
Vendored
+6
@@ -80,6 +80,11 @@ declare module 'expo-file-system' {
|
||||
slice(start?: number, end?: number, contentType?: string): Blob
|
||||
upload(url: string, options?: UploadOptions): Promise<UploadResult>
|
||||
createUploadTask(url: string, options?: UploadOptions): UploadTask
|
||||
static downloadFileAsync(
|
||||
url: string,
|
||||
destination: File | Directory,
|
||||
options?: DownloadOptions,
|
||||
): Promise<File>
|
||||
static createDownloadTask(
|
||||
url: string,
|
||||
destination: File | Directory,
|
||||
@@ -92,6 +97,7 @@ declare module 'expo-file-system' {
|
||||
}
|
||||
|
||||
export class Directory extends ExpoFileSystemDirectory {
|
||||
static pickDirectoryAsync(initialUri?: string): Promise<Directory>
|
||||
constructor(...uris: (string | File | Directory)[])
|
||||
get parentDirectory(): Directory
|
||||
list(): (Directory | File)[]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {Platform} from 'react-native'
|
||||
import {setStringAsync} from 'expo-clipboard'
|
||||
import * as FileSystem from 'expo-file-system/legacy'
|
||||
import {Paths} from 'expo-file-system'
|
||||
import {Image} from 'expo-image'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
@@ -41,15 +41,13 @@ export function AboutSettingsScreen({}: Props) {
|
||||
const {mutate: onClearImageCache, isPending: isClearingImageCache} =
|
||||
useMutation({
|
||||
mutationFn: async () => {
|
||||
const freeSpaceBefore = await FileSystem.getFreeDiskStorageAsync()
|
||||
await Promise.all([
|
||||
// expo-image's disk cache
|
||||
Image.clearDiskCache(),
|
||||
// full-resolution media-upload leftovers (picker/manipulator copies);
|
||||
// the only in-app way for iOS users to reclaim this space
|
||||
purgeTemporaryImageFiles(),
|
||||
])
|
||||
const freeSpaceAfter = await FileSystem.getFreeDiskStorageAsync()
|
||||
const freeSpaceBefore = Paths.availableDiskSpace
|
||||
// full-resolution media-upload leftovers (picker/manipulator copies);
|
||||
// the only in-app way for iOS users to reclaim this space
|
||||
purgeTemporaryImageFiles()
|
||||
// expo-image's disk cache
|
||||
await Image.clearDiskCache()
|
||||
const freeSpaceAfter = Paths.availableDiskSpace
|
||||
const spaceDiff = freeSpaceBefore - freeSpaceAfter
|
||||
return spaceDiff * -1
|
||||
},
|
||||
|
||||
+42
-43
@@ -1,10 +1,4 @@
|
||||
import {
|
||||
cacheDirectory,
|
||||
copyAsync,
|
||||
deleteAsync,
|
||||
makeDirectoryAsync,
|
||||
moveAsync,
|
||||
} from 'expo-file-system/legacy'
|
||||
import {Directory, File, Paths} from 'expo-file-system'
|
||||
import {type ImageManipulatorContext, SaveFormat} from 'expo-image-manipulator'
|
||||
import {nanoid} from 'nanoid/non-secure'
|
||||
|
||||
@@ -50,11 +44,14 @@ type ComposerImageWithTransformation = ComposerImageBase & {
|
||||
export type ComposerImage =
|
||||
ComposerImageWithoutTransformation | ComposerImageWithTransformation
|
||||
|
||||
let _imageCacheDirectory: string
|
||||
let _imageCacheDirectory: Directory
|
||||
|
||||
function getImageCacheDirectory(): string | null {
|
||||
function getImageCacheDirectory(): Directory | null {
|
||||
if (IS_NATIVE) {
|
||||
return (_imageCacheDirectory ??= joinPath(cacheDirectory!, 'bsky-composer'))
|
||||
return (_imageCacheDirectory ??= new Directory(
|
||||
Paths.cache,
|
||||
'bsky-composer',
|
||||
))
|
||||
}
|
||||
|
||||
return null
|
||||
@@ -273,13 +270,14 @@ export async function compressImage(
|
||||
async function moveIfNecessary(from: string) {
|
||||
const cacheDir = IS_NATIVE && getImageCacheDirectory()
|
||||
|
||||
if (cacheDir && !from.startsWith(cacheDir)) {
|
||||
const to = joinPath(cacheDir, nanoid(36))
|
||||
if (cacheDir && !from.startsWith(cacheDir.uri)) {
|
||||
const source = new File(normalizeFileUri(from))
|
||||
const destination = new File(cacheDir, nanoid(36))
|
||||
|
||||
await makeDirectoryAsync(cacheDir, {intermediates: true})
|
||||
await moveAsync({from, to})
|
||||
cacheDir.create({idempotent: true, intermediates: true})
|
||||
await source.move(destination)
|
||||
|
||||
return to
|
||||
return destination.uri
|
||||
}
|
||||
|
||||
return from
|
||||
@@ -316,20 +314,15 @@ async function copyToCache(from: string): Promise<string> {
|
||||
|
||||
// Native: copy to cache directory to survive OS temp file cleanup
|
||||
const cacheDir = getImageCacheDirectory()
|
||||
if (!cacheDir || from.startsWith(cacheDir)) {
|
||||
if (!cacheDir || from.startsWith(cacheDir.uri)) {
|
||||
return from
|
||||
}
|
||||
|
||||
const to = joinPath(cacheDir, nanoid(36))
|
||||
await makeDirectoryAsync(cacheDir, {intermediates: true})
|
||||
const destination = new File(cacheDir, nanoid(36))
|
||||
cacheDir.create({idempotent: true, intermediates: true})
|
||||
|
||||
let normalizedFrom = from
|
||||
if (!from.startsWith('file://') && from.startsWith('/')) {
|
||||
normalizedFrom = `file://${from}`
|
||||
}
|
||||
|
||||
await copyAsync({from: normalizedFrom, to})
|
||||
return to
|
||||
await new File(normalizeFileUri(from)).copy(destination)
|
||||
return destination.uri
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -363,36 +356,42 @@ function blobToDataUri(blob: Blob): Promise<string> {
|
||||
const SYSTEM_MEDIA_CACHE_DIRS = ['ImagePicker', 'ImageManipulator']
|
||||
|
||||
/** Purge files that were created to accomodate image manipulation */
|
||||
export async function purgeTemporaryImageFiles() {
|
||||
export function purgeTemporaryImageFiles() {
|
||||
if (!IS_NATIVE) {
|
||||
return
|
||||
}
|
||||
|
||||
const cacheDir = getImageCacheDirectory()
|
||||
if (cacheDir) {
|
||||
await deleteAsync(cacheDir, {idempotent: true})
|
||||
await makeDirectoryAsync(cacheDir)
|
||||
try {
|
||||
if (cacheDir.exists) {
|
||||
cacheDir.delete()
|
||||
}
|
||||
cacheDir.create()
|
||||
} catch (err) {
|
||||
logger.warn('Failed to purge composer image cache', {safeMessage: err})
|
||||
}
|
||||
}
|
||||
|
||||
// We don't recreate these - the respective expo modules recreate them on
|
||||
// demand the next time they run.
|
||||
await Promise.all(
|
||||
SYSTEM_MEDIA_CACHE_DIRS.map(dir =>
|
||||
deleteAsync(joinPath(cacheDirectory!, dir), {idempotent: true}),
|
||||
),
|
||||
)
|
||||
for (const dir of SYSTEM_MEDIA_CACHE_DIRS) {
|
||||
try {
|
||||
const directory = new Directory(Paths.cache, dir)
|
||||
if (directory.exists) {
|
||||
directory.delete()
|
||||
}
|
||||
} catch (err) {
|
||||
logger.warn('Failed to purge system image cache', {
|
||||
safeMessage: err,
|
||||
directory: dir,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function joinPath(a: string, b: string) {
|
||||
if (a.endsWith('/')) {
|
||||
if (b.startsWith('/')) {
|
||||
return a.slice(0, -1) + b
|
||||
}
|
||||
return a + b
|
||||
} else if (b.startsWith('/')) {
|
||||
return a + b
|
||||
}
|
||||
return a + '/' + b
|
||||
function normalizeFileUri(uri: string): string {
|
||||
return uri.startsWith('/') ? `file://${uri}` : uri
|
||||
}
|
||||
|
||||
function containImageRes(
|
||||
|
||||
Reference in New Issue
Block a user