attempt to fix blob mangement

This commit is contained in:
Samuel Newman
2026-01-16 10:52:47 +02:00
parent 666e1318b2
commit 476a01a78f
3 changed files with 114 additions and 5 deletions
+10 -2
View File
@@ -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
+36 -1
View File
@@ -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
View File
@@ -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()