Bluesky app fork with some witchin' additions 馃挮
at main 171 lines 4.5 kB view raw
1/** 2 * Native file system storage for draft media. 3 * Media is stored by localRefPath key (unique identifier stored in server draft). 4 */ 5import {Directory, File, Paths} from 'expo-file-system' 6 7import {logger} from './logger' 8 9const MEDIA_DIR = 'bsky-draft-media' 10 11function getMediaDirectory(): Directory { 12 return new Directory(Paths.document, MEDIA_DIR) 13} 14 15function getMediaFile(localRefPath: string): File { 16 const safeFilename = encodeURIComponent(localRefPath) 17 return new File(getMediaDirectory(), safeFilename) 18} 19 20let dirCreated = false 21 22/** 23 * Ensure the media directory exists 24 */ 25function ensureDirectory(): void { 26 if (dirCreated) return 27 const dir = getMediaDirectory() 28 if (!dir.exists) { 29 dir.create() 30 } 31 dirCreated = true 32} 33 34/** 35 * Save a media file to local storage by localRefPath key 36 */ 37export async function saveMediaToLocal( 38 localRefPath: string, 39 sourcePath: string, 40): Promise<void> { 41 ensureDirectory() 42 43 const destFile = getMediaFile(localRefPath) 44 45 // Ensure source path has file:// prefix for expo-file-system 46 let normalizedSource = sourcePath 47 if (!sourcePath.startsWith('file://') && sourcePath.startsWith('/')) { 48 normalizedSource = `file://${sourcePath}` 49 } 50 51 try { 52 const sourceFile = new File(normalizedSource) 53 sourceFile.copy(destFile) 54 // Update cache after successful save 55 mediaExistsCache.set(localRefPath, true) 56 } catch (error) { 57 logger.error('Failed to save media to drafts storage', { 58 error, 59 localRefPath, 60 sourcePath: normalizedSource, 61 destPath: destFile.uri, 62 }) 63 throw error 64 } 65} 66 67/** 68 * Load a media file path from local storage 69 * @returns The file URI for the saved media 70 */ 71export async function loadMediaFromLocal( 72 localRefPath: string, 73): Promise<string> { 74 const file = getMediaFile(localRefPath) 75 76 if (!file.exists) { 77 throw new Error(`Media file not found: ${localRefPath}`) 78 } 79 80 return file.uri 81} 82 83/** 84 * Delete a media file from local storage 85 */ 86export async function deleteMediaFromLocal( 87 localRefPath: string, 88): Promise<void> { 89 const file = getMediaFile(localRefPath) 90 // Idempotent: only delete if file exists 91 if (file.exists) { 92 file.delete() 93 } 94 mediaExistsCache.delete(localRefPath) 95} 96 97/** 98 * Check if a media file exists in local storage (synchronous check using cache) 99 * Note: This uses a cached directory listing for performance 100 */ 101const mediaExistsCache = new Map<string, boolean>() 102let cachePopulated = false 103 104export function mediaExists(localRefPath: string): boolean { 105 // For native, we need an async check but the API requires sync 106 // Use cached result if available, otherwise assume doesn't exist 107 if (mediaExistsCache.has(localRefPath)) { 108 return mediaExistsCache.get(localRefPath)! 109 } 110 // If cache not populated yet, trigger async population 111 if (!cachePopulated && !populateCachePromise) { 112 populateCachePromise = populateCacheInternal() 113 } 114 return false // Conservative: assume doesn't exist if not in cache 115} 116 117let populateCachePromise: Promise<void> | null = null 118 119function populateCacheInternal(): Promise<void> { 120 return new Promise(resolve => { 121 try { 122 const dir = getMediaDirectory() 123 if (dir.exists) { 124 const items = dir.list() 125 for (const item of items) { 126 // Reverse the URL encoding to get the original localRefPath 127 const localRefPath = decodeURIComponent(item.name) 128 mediaExistsCache.set(localRefPath, true) 129 } 130 } 131 cachePopulated = true 132 } catch (e) { 133 logger.warn('Failed to populate media cache', {error: e}) 134 } 135 resolve() 136 }) 137} 138 139/** 140 * Ensure the media cache is populated. Call this before checking mediaExists. 141 */ 142export async function ensureMediaCachePopulated(): Promise<void> { 143 if (cachePopulated) return 144 if (!populateCachePromise) { 145 populateCachePromise = populateCacheInternal() 146 } 147 await populateCachePromise 148} 149 150/** 151 * Clear the media exists cache (call when media is added/deleted) 152 */ 153export function clearMediaCache(): void { 154 mediaExistsCache.clear() 155 cachePopulated = false 156 populateCachePromise = null 157} 158 159/** 160 * Revoke a media URL (no-op on native - only needed for web blob URLs) 161 */ 162export function revokeMediaUrl(_url: string): void { 163 // No-op on native - file URIs don't need revocation 164} 165 166/** 167 * Revoke all media URLs (no-op on native - only needed for web blob URLs) 168 */ 169export function revokeAllMediaUrls(): void { 170 // No-op on native - file URIs don't need revocation 171}