Migrate storage.ts from expo-file-system/legacy to expo-file-system
Use the new object-based expo-file-system API (SDK 54+) with Directory and File classes instead of the legacy function-based API. The new API provides synchronous operations for file/directory existence checks, creation, copying, deletion, and listing. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
+55
-48
@@ -2,30 +2,20 @@
|
|||||||
* Native file system storage for draft media.
|
* Native file system storage for draft media.
|
||||||
* Media is stored by localRefPath key (unique identifier stored in server draft).
|
* Media is stored by localRefPath key (unique identifier stored in server draft).
|
||||||
*/
|
*/
|
||||||
import {
|
import {Directory, File, Paths} from 'expo-file-system'
|
||||||
copyAsync,
|
|
||||||
deleteAsync,
|
|
||||||
documentDirectory,
|
|
||||||
getInfoAsync,
|
|
||||||
makeDirectoryAsync,
|
|
||||||
} from 'expo-file-system/legacy'
|
|
||||||
|
|
||||||
import {logger} from '#/logger'
|
import {logger} from './logger'
|
||||||
|
|
||||||
const MEDIA_DIR = 'bsky-draft-media'
|
const MEDIA_DIR = 'bsky-draft-media'
|
||||||
|
|
||||||
function joinPath(...segments: string[]): string {
|
function getMediaDirectory(): Directory {
|
||||||
return segments.join('/').replace(/\/+/g, '/')
|
return new Directory(Paths.document, MEDIA_DIR)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMediaDirectory(): string {
|
function getMediaFile(localRefPath: string): File {
|
||||||
return joinPath(documentDirectory!, MEDIA_DIR)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMediaPath(localRefPath: string): string {
|
|
||||||
// Use localRefPath as filename (replace unsafe chars)
|
// Use localRefPath as filename (replace unsafe chars)
|
||||||
const safeFilename = localRefPath.replace(/[/:]/g, '_')
|
const safeFilename = localRefPath.replace(/[/:]/g, '_')
|
||||||
return joinPath(getMediaDirectory(), safeFilename)
|
return new File(getMediaDirectory(), safeFilename)
|
||||||
}
|
}
|
||||||
|
|
||||||
let dirCreated = false
|
let dirCreated = false
|
||||||
@@ -33,22 +23,25 @@ let dirCreated = false
|
|||||||
/**
|
/**
|
||||||
* Ensure the media directory exists
|
* Ensure the media directory exists
|
||||||
*/
|
*/
|
||||||
async function ensureDirectory(): Promise<void> {
|
function ensureDirectory(): void {
|
||||||
if (dirCreated) return
|
if (dirCreated) return
|
||||||
await makeDirectoryAsync(getMediaDirectory(), {intermediates: true})
|
const dir = getMediaDirectory()
|
||||||
|
if (!dir.exists) {
|
||||||
|
dir.create()
|
||||||
|
}
|
||||||
dirCreated = true
|
dirCreated = true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save a media file to local storage by localRefPath key
|
* Save a media file to local storage by localRefPath key
|
||||||
*/
|
*/
|
||||||
export async function saveMediaToLocal(
|
export function saveMediaToLocal(
|
||||||
localRefPath: string,
|
localRefPath: string,
|
||||||
sourcePath: string,
|
sourcePath: string,
|
||||||
): Promise<void> {
|
): void {
|
||||||
await ensureDirectory()
|
ensureDirectory()
|
||||||
|
|
||||||
const destPath = getMediaPath(localRefPath)
|
const destFile = getMediaFile(localRefPath)
|
||||||
|
|
||||||
// Ensure source path has file:// prefix for expo-file-system
|
// Ensure source path has file:// prefix for expo-file-system
|
||||||
let normalizedSource = sourcePath
|
let normalizedSource = sourcePath
|
||||||
@@ -57,7 +50,8 @@ export async function saveMediaToLocal(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await copyAsync({from: normalizedSource, to: destPath})
|
const sourceFile = new File(normalizedSource)
|
||||||
|
sourceFile.copy(destFile)
|
||||||
// Update cache after successful save
|
// Update cache after successful save
|
||||||
mediaExistsCache.set(localRefPath, true)
|
mediaExistsCache.set(localRefPath, true)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -65,7 +59,7 @@ export async function saveMediaToLocal(
|
|||||||
error,
|
error,
|
||||||
localRefPath,
|
localRefPath,
|
||||||
sourcePath: normalizedSource,
|
sourcePath: normalizedSource,
|
||||||
destPath,
|
destPath: destFile.uri,
|
||||||
})
|
})
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
@@ -73,29 +67,27 @@ export async function saveMediaToLocal(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Load a media file path from local storage
|
* Load a media file path from local storage
|
||||||
* @returns The file path for the saved media
|
* @returns The file URI for the saved media
|
||||||
*/
|
*/
|
||||||
export async function loadMediaFromLocal(
|
export function loadMediaFromLocal(localRefPath: string): string {
|
||||||
localRefPath: string,
|
const file = getMediaFile(localRefPath)
|
||||||
): Promise<string> {
|
|
||||||
const path = getMediaPath(localRefPath)
|
|
||||||
const info = await getInfoAsync(path)
|
|
||||||
|
|
||||||
if (!info.exists) {
|
if (!file.exists) {
|
||||||
throw new Error(`Media file not found: ${localRefPath}`)
|
throw new Error(`Media file not found: ${localRefPath}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return path
|
return file.uri
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a media file from local storage
|
* Delete a media file from local storage
|
||||||
*/
|
*/
|
||||||
export async function deleteMediaFromLocal(
|
export function deleteMediaFromLocal(localRefPath: string): void {
|
||||||
localRefPath: string,
|
const file = getMediaFile(localRefPath)
|
||||||
): Promise<void> {
|
// Idempotent: only delete if file exists
|
||||||
const path = getMediaPath(localRefPath)
|
if (file.exists) {
|
||||||
await deleteAsync(path, {idempotent: true})
|
file.delete()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -107,27 +99,28 @@ let cachePopulated = false
|
|||||||
|
|
||||||
export function mediaExists(localRefPath: string): boolean {
|
export function mediaExists(localRefPath: string): boolean {
|
||||||
// For native, we need an async check but the API requires sync
|
// For native, we need an async check but the API requires sync
|
||||||
// Use cached result if available, otherwise assume exists (will fail on load if not)
|
// Use cached result if available, otherwise assume doesn't exist
|
||||||
if (mediaExistsCache.has(localRefPath)) {
|
if (mediaExistsCache.has(localRefPath)) {
|
||||||
return mediaExistsCache.get(localRefPath)!
|
return mediaExistsCache.get(localRefPath)!
|
||||||
}
|
}
|
||||||
// If cache not populated yet, trigger async population and return true optimistically
|
// If cache not populated yet, trigger async population
|
||||||
if (!cachePopulated) {
|
if (!cachePopulated && !populateCachePromise) {
|
||||||
populateCache()
|
populateCachePromise = populateCacheInternal()
|
||||||
}
|
}
|
||||||
return false // Conservative: assume doesn't exist if not in cache
|
return false // Conservative: assume doesn't exist if not in cache
|
||||||
}
|
}
|
||||||
|
|
||||||
async function populateCache(): Promise<void> {
|
let populateCachePromise: Promise<void> | null = null
|
||||||
|
|
||||||
|
function populateCacheInternal(): Promise<void> {
|
||||||
|
return new Promise(resolve => {
|
||||||
try {
|
try {
|
||||||
const {readDirectoryAsync} = await import('expo-file-system/legacy')
|
|
||||||
const dir = getMediaDirectory()
|
const dir = getMediaDirectory()
|
||||||
const info = await getInfoAsync(dir)
|
if (dir.exists) {
|
||||||
if (info.exists) {
|
const items = dir.list()
|
||||||
const files = await readDirectoryAsync(dir)
|
for (const item of items) {
|
||||||
for (const file of files) {
|
|
||||||
// Reverse the safe filename transformation
|
// Reverse the safe filename transformation
|
||||||
const localRefPath = file.replace(/_/g, ':').replace(/_/g, '/')
|
const localRefPath = item.name.replace(/_/g, ':').replace(/_/g, '/')
|
||||||
mediaExistsCache.set(localRefPath, true)
|
mediaExistsCache.set(localRefPath, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,6 +128,19 @@ async function populateCache(): Promise<void> {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.warn('Failed to populate media cache', {error: e})
|
logger.warn('Failed to populate media cache', {error: e})
|
||||||
}
|
}
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure the media cache is populated. Call this before checking mediaExists.
|
||||||
|
*/
|
||||||
|
export async function ensureMediaCachePopulated(): Promise<void> {
|
||||||
|
if (cachePopulated) return
|
||||||
|
if (!populateCachePromise) {
|
||||||
|
populateCachePromise = populateCacheInternal()
|
||||||
|
}
|
||||||
|
await populateCachePromise
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -143,4 +149,5 @@ async function populateCache(): Promise<void> {
|
|||||||
export function clearMediaCache(): void {
|
export function clearMediaCache(): void {
|
||||||
mediaExistsCache.clear()
|
mediaExistsCache.clear()
|
||||||
cachePopulated = false
|
cachePopulated = false
|
||||||
|
populateCachePromise = null
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user