attempt to fix blob mangement
This commit is contained in:
@@ -50,13 +50,21 @@ export async function saveMediaToLocal(
|
|||||||
|
|
||||||
const destPath = getMediaPath(localRefPath)
|
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 {
|
try {
|
||||||
await copyAsync({from: sourcePath, to: destPath})
|
await copyAsync({from: normalizedSource, to: destPath})
|
||||||
|
// Update cache after successful save
|
||||||
|
mediaExistsCache.set(localRefPath, true)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to save media to drafts storage', {
|
logger.error('Failed to save media to drafts storage', {
|
||||||
error,
|
error,
|
||||||
localRefPath,
|
localRefPath,
|
||||||
sourcePath,
|
sourcePath: normalizedSource,
|
||||||
destPath,
|
destPath,
|
||||||
})
|
})
|
||||||
throw error
|
throw error
|
||||||
|
|||||||
@@ -38,7 +38,31 @@ async function getDB(): Promise<IDBPDatabase<DraftMediaDB>> {
|
|||||||
* Convert a path/URL to a Blob
|
* Convert a path/URL to a Blob
|
||||||
*/
|
*/
|
||||||
async function toBlob(sourcePath: string): Promise<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)
|
const response = await fetch(sourcePath)
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch media: ${response.status}`)
|
||||||
|
}
|
||||||
return response.blob()
|
return response.blob()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,7 +74,18 @@ export async function saveMediaToLocal(
|
|||||||
sourcePath: string,
|
sourcePath: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const db = await getDB()
|
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 {
|
try {
|
||||||
await db.put(
|
await db.put(
|
||||||
|
|||||||
+68
-2
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
cacheDirectory,
|
cacheDirectory,
|
||||||
|
copyAsync,
|
||||||
deleteAsync,
|
deleteAsync,
|
||||||
makeDirectoryAsync,
|
makeDirectoryAsync,
|
||||||
moveAsync,
|
moveAsync,
|
||||||
@@ -18,7 +19,7 @@ import {openCropper} from '#/lib/media/picker'
|
|||||||
import {type PickerImage} from '#/lib/media/picker.shared'
|
import {type PickerImage} from '#/lib/media/picker.shared'
|
||||||
import {getDataUriSize} from '#/lib/media/util'
|
import {getDataUriSize} from '#/lib/media/util'
|
||||||
import {isCancelledError} from '#/lib/strings/errors'
|
import {isCancelledError} from '#/lib/strings/errors'
|
||||||
import {IS_NATIVE} from '#/env'
|
import {IS_NATIVE, IS_WEB} from '#/env'
|
||||||
|
|
||||||
export type ImageTransformation = {
|
export type ImageTransformation = {
|
||||||
crop?: ActionCrop['crop']
|
crop?: ActionCrop['crop']
|
||||||
@@ -69,7 +70,8 @@ export async function createComposerImage(
|
|||||||
alt: '',
|
alt: '',
|
||||||
source: {
|
source: {
|
||||||
id: nanoid(),
|
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,
|
width: raw.width,
|
||||||
height: raw.height,
|
height: raw.height,
|
||||||
mime: raw.mime,
|
mime: raw.mime,
|
||||||
@@ -258,6 +260,70 @@ async function moveIfNecessary(from: string) {
|
|||||||
return from
|
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 */
|
/** Purge files that were created to accomodate image manipulation */
|
||||||
export async function purgeTemporaryImageFiles() {
|
export async function purgeTemporaryImageFiles() {
|
||||||
const cacheDir = IS_NATIVE && getImageCacheDirectory()
|
const cacheDir = IS_NATIVE && getImageCacheDirectory()
|
||||||
|
|||||||
Reference in New Issue
Block a user