attempt to fix blob mangement
This commit is contained in:
@@ -50,13 +50,21 @@ export async function saveMediaToLocal(
|
||||
|
||||
const destPath = getMediaPath(localRefPath)
|
||||
|
||||
// Ensure source path has file:// prefix for expo-file-system
|
||||
let normalizedSource = sourcePath
|
||||
if (!sourcePath.startsWith('file://') && sourcePath.startsWith('/')) {
|
||||
normalizedSource = `file://${sourcePath}`
|
||||
}
|
||||
|
||||
try {
|
||||
await copyAsync({from: sourcePath, to: destPath})
|
||||
await copyAsync({from: normalizedSource, to: destPath})
|
||||
// Update cache after successful save
|
||||
mediaExistsCache.set(localRefPath, true)
|
||||
} catch (error) {
|
||||
logger.error('Failed to save media to drafts storage', {
|
||||
error,
|
||||
localRefPath,
|
||||
sourcePath,
|
||||
sourcePath: normalizedSource,
|
||||
destPath,
|
||||
})
|
||||
throw error
|
||||
|
||||
@@ -38,7 +38,31 @@ async function getDB(): Promise<IDBPDatabase<DraftMediaDB>> {
|
||||
* Convert a path/URL to a Blob
|
||||
*/
|
||||
async function toBlob(sourcePath: string): Promise<Blob> {
|
||||
// Handle data URIs directly
|
||||
if (sourcePath.startsWith('data:')) {
|
||||
const response = await fetch(sourcePath)
|
||||
return response.blob()
|
||||
}
|
||||
|
||||
// Handle blob URLs
|
||||
if (sourcePath.startsWith('blob:')) {
|
||||
try {
|
||||
const response = await fetch(sourcePath)
|
||||
return response.blob()
|
||||
} catch (e) {
|
||||
logger.error('Failed to fetch blob URL - it may have been revoked', {
|
||||
error: e,
|
||||
sourcePath,
|
||||
})
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
// Handle regular URLs
|
||||
const response = await fetch(sourcePath)
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch media: ${response.status}`)
|
||||
}
|
||||
return response.blob()
|
||||
}
|
||||
|
||||
@@ -50,7 +74,18 @@ export async function saveMediaToLocal(
|
||||
sourcePath: string,
|
||||
): Promise<void> {
|
||||
const db = await getDB()
|
||||
const blob = await toBlob(sourcePath)
|
||||
|
||||
let blob: Blob
|
||||
try {
|
||||
blob = await toBlob(sourcePath)
|
||||
} catch (error) {
|
||||
logger.error('Failed to convert source to blob', {
|
||||
error,
|
||||
localRefPath,
|
||||
sourcePath,
|
||||
})
|
||||
throw error
|
||||
}
|
||||
|
||||
try {
|
||||
await db.put(
|
||||
|
||||
+68
-2
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
cacheDirectory,
|
||||
copyAsync,
|
||||
deleteAsync,
|
||||
makeDirectoryAsync,
|
||||
moveAsync,
|
||||
@@ -18,7 +19,7 @@ import {openCropper} from '#/lib/media/picker'
|
||||
import {type PickerImage} from '#/lib/media/picker.shared'
|
||||
import {getDataUriSize} from '#/lib/media/util'
|
||||
import {isCancelledError} from '#/lib/strings/errors'
|
||||
import {IS_NATIVE} from '#/env'
|
||||
import {IS_NATIVE, IS_WEB} from '#/env'
|
||||
|
||||
export type ImageTransformation = {
|
||||
crop?: ActionCrop['crop']
|
||||
@@ -69,7 +70,8 @@ export async function createComposerImage(
|
||||
alt: '',
|
||||
source: {
|
||||
id: nanoid(),
|
||||
path: await moveIfNecessary(raw.path),
|
||||
// Copy to cache to ensure file survives OS temporary file cleanup
|
||||
path: await copyToCache(raw.path),
|
||||
width: raw.width,
|
||||
height: raw.height,
|
||||
mime: raw.mime,
|
||||
@@ -258,6 +260,70 @@ async function moveIfNecessary(from: string) {
|
||||
return from
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy a file from a potentially temporary location to our cache directory.
|
||||
* This ensures picker files are available for draft saving even if the original
|
||||
* temporary files are cleaned up by the OS.
|
||||
*
|
||||
* On web, converts blob URLs to data URIs immediately to prevent revocation issues.
|
||||
*/
|
||||
async function copyToCache(from: string): Promise<string> {
|
||||
// Handle web blob URLs - convert to data URI immediately before they can be revoked
|
||||
if (IS_WEB && from.startsWith('blob:')) {
|
||||
try {
|
||||
const response = await fetch(from)
|
||||
const blob = await response.blob()
|
||||
return await blobToDataUri(blob)
|
||||
} catch (e) {
|
||||
// If fetch fails, the blob URL was likely already revoked
|
||||
// Return as-is and let downstream code handle the error
|
||||
return from
|
||||
}
|
||||
}
|
||||
|
||||
// Data URIs don't need any conversion
|
||||
if (from.startsWith('data:')) {
|
||||
return from
|
||||
}
|
||||
|
||||
const cacheDir = IS_WEB && getImageCacheDirectory()
|
||||
|
||||
// On web (non-blob URLs) or if already in cache dir, no need to copy
|
||||
if (!cacheDir || from.startsWith(cacheDir)) {
|
||||
return from
|
||||
}
|
||||
|
||||
const to = joinPath(cacheDir, nanoid(36))
|
||||
await makeDirectoryAsync(cacheDir, {intermediates: true})
|
||||
|
||||
// Normalize the source path for expo-file-system
|
||||
let normalizedFrom = from
|
||||
if (!from.startsWith('file://') && from.startsWith('/')) {
|
||||
normalizedFrom = `file://${from}`
|
||||
}
|
||||
|
||||
await copyAsync({from: normalizedFrom, to})
|
||||
return to
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a Blob to a data URI
|
||||
*/
|
||||
function blobToDataUri(blob: Blob): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.onloadend = () => {
|
||||
if (typeof reader.result === 'string') {
|
||||
resolve(reader.result)
|
||||
} else {
|
||||
reject(new Error('Failed to convert blob to data URI'))
|
||||
}
|
||||
}
|
||||
reader.onerror = () => reject(reader.error)
|
||||
reader.readAsDataURL(blob)
|
||||
})
|
||||
}
|
||||
|
||||
/** Purge files that were created to accomodate image manipulation */
|
||||
export async function purgeTemporaryImageFiles() {
|
||||
const cacheDir = IS_NATIVE && getImageCacheDirectory()
|
||||
|
||||
Reference in New Issue
Block a user