1import { Platform } from 'expo-modules-core'; 2import { getAssetByID } from 'react-native/Libraries/Image/AssetRegistry'; 3 4import { AssetMetadata, selectAssetSource } from './AssetSources'; 5import * as AssetUris from './AssetUris'; 6import * as ImageAssets from './ImageAssets'; 7import { getLocalAssetUri } from './LocalAssets'; 8import { downloadAsync, IS_ENV_WITH_UPDATES_ENABLED } from './PlatformUtils'; 9import resolveAssetSource from './resolveAssetSource'; 10 11// @docsMissing 12export type AssetDescriptor = { 13 name: string; 14 type: string; 15 hash?: string | null; 16 uri: string; 17 width?: number | null; 18 height?: number | null; 19}; 20 21type DownloadPromiseCallbacks = { 22 resolve: () => void; 23 reject: (error: Error) => void; 24}; 25 26export { AssetMetadata }; 27 28// @needsAudit 29/** 30 * The `Asset` class represents an asset in your app. It gives metadata about the asset (such as its 31 * name and type) and provides facilities to load the asset data. 32 */ 33export class Asset { 34 /** 35 * @private 36 */ 37 static byHash = {}; 38 /** 39 * @private 40 */ 41 static byUri = {}; 42 43 /** 44 * The name of the asset file without the extension. Also without the part from `@` onward in the 45 * filename (used to specify scale factor for images). 46 */ 47 name: string; 48 /** 49 * The extension of the asset filename. 50 */ 51 type: string; 52 /** 53 * The MD5 hash of the asset's data. 54 */ 55 hash: string | null = null; 56 /** 57 * A URI that points to the asset's data on the remote server. When running the published version 58 * of your app, this refers to the location on Expo's asset server where Expo has stored your 59 * asset. When running the app from Expo CLI during development, this URI points to Expo CLI's 60 * server running on your computer and the asset is served directly from your computer. If you 61 * are not using Classic Updates (legacy), this field should be ignored as we ensure your assets 62 * are on device before before running your application logic. 63 */ 64 uri: string; 65 /** 66 * If the asset has been downloaded (by calling [`downloadAsync()`](#downloadasync)), the 67 * `file://` URI pointing to the local file on the device that contains the asset data. 68 */ 69 localUri: string | null = null; 70 /** 71 * If the asset is an image, the width of the image data divided by the scale factor. The scale 72 * factor is the number after `@` in the filename, or `1` if not present. 73 */ 74 width: number | null = null; 75 /** 76 * If the asset is an image, the height of the image data divided by the scale factor. The scale factor is the number after `@` in the filename, or `1` if not present. 77 */ 78 height: number | null = null; 79 // @docsMissing 80 downloading: boolean = false; 81 // @docsMissing 82 downloaded: boolean = false; 83 84 /** 85 * @private 86 */ 87 _downloadCallbacks: DownloadPromiseCallbacks[] = []; 88 89 constructor({ name, type, hash = null, uri, width, height }: AssetDescriptor) { 90 this.name = name; 91 this.type = type; 92 this.hash = hash; 93 this.uri = uri; 94 95 if (typeof width === 'number') { 96 this.width = width; 97 } 98 if (typeof height === 'number') { 99 this.height = height; 100 } 101 102 if (hash) { 103 this.localUri = getLocalAssetUri(hash, type); 104 if (this.localUri) { 105 this.downloaded = true; 106 } 107 } 108 109 if (Platform.OS === 'web') { 110 if (!name) { 111 this.name = AssetUris.getFilename(uri); 112 } 113 if (!type) { 114 this.type = AssetUris.getFileExtension(uri); 115 } 116 } 117 } 118 119 // @needsAudit 120 /** 121 * A helper that wraps `Asset.fromModule(module).downloadAsync` for convenience. 122 * @param moduleId An array of `require('path/to/file')` or external network URLs. Can also be 123 * just one module or URL without an Array. 124 * @return Returns a Promise that fulfills with an array of `Asset`s when the asset(s) has been 125 * saved to disk. 126 * @example 127 * ```ts 128 * const [{ localUri }] = await Asset.loadAsync(require('./assets/snack-icon.png')); 129 * ``` 130 */ 131 static loadAsync(moduleId: number | number[] | string | string[]): Promise<Asset[]> { 132 const moduleIds = Array.isArray(moduleId) ? moduleId : [moduleId]; 133 return Promise.all(moduleIds.map((moduleId) => Asset.fromModule(moduleId).downloadAsync())); 134 } 135 136 // @needsAudit 137 /** 138 * Returns the [`Asset`](#asset) instance representing an asset given its module or URL. 139 * @param virtualAssetModule The value of `require('path/to/file')` for the asset or external 140 * network URL 141 * @return The [`Asset`](#asset) instance for the asset. 142 */ 143 static fromModule(virtualAssetModule: number | string): Asset { 144 if (typeof virtualAssetModule === 'string') { 145 return Asset.fromURI(virtualAssetModule); 146 } 147 148 const meta = getAssetByID(virtualAssetModule); 149 if (!meta) { 150 throw new Error(`Module "${virtualAssetModule}" is missing from the asset registry`); 151 } 152 153 // Outside of the managed env we need the moduleId to initialize the asset 154 // because resolveAssetSource depends on it 155 if (!IS_ENV_WITH_UPDATES_ENABLED) { 156 const { uri } = resolveAssetSource(virtualAssetModule); 157 const asset = new Asset({ 158 name: meta.name, 159 type: meta.type, 160 hash: meta.hash, 161 uri, 162 width: meta.width, 163 height: meta.height, 164 }); 165 166 // TODO: FileSystem should probably support 'downloading' from drawable 167 // resources But for now it doesn't (it only supports raw resources) and 168 // React Native's Image works fine with drawable resource names for 169 // images. 170 if (Platform.OS === 'android' && !uri.includes(':') && (meta.width || meta.height)) { 171 asset.localUri = asset.uri; 172 asset.downloaded = true; 173 } 174 175 Asset.byHash[meta.hash] = asset; 176 return asset; 177 } 178 179 return Asset.fromMetadata(meta); 180 } 181 182 // @docsMissing 183 static fromMetadata(meta: AssetMetadata): Asset { 184 // The hash of the whole asset, not to be confused with the hash of a specific file returned 185 // from `selectAssetSource` 186 const metaHash = meta.hash; 187 if (Asset.byHash[metaHash]) { 188 return Asset.byHash[metaHash]; 189 } 190 191 const { uri, hash } = selectAssetSource(meta); 192 const asset = new Asset({ 193 name: meta.name, 194 type: meta.type, 195 hash, 196 uri, 197 width: meta.width, 198 height: meta.height, 199 }); 200 Asset.byHash[metaHash] = asset; 201 return asset; 202 } 203 204 // @docsMissing 205 static fromURI(uri: string): Asset { 206 if (Asset.byUri[uri]) { 207 return Asset.byUri[uri]; 208 } 209 210 // Possibly a Base64-encoded URI 211 let type = ''; 212 if (uri.indexOf(';base64') > -1) { 213 type = uri.split(';')[0].split('/')[1]; 214 } else { 215 const extension = AssetUris.getFileExtension(uri); 216 type = extension.startsWith('.') ? extension.substring(1) : extension; 217 } 218 219 const asset = new Asset({ 220 name: '', 221 type, 222 hash: null, 223 uri, 224 }); 225 226 Asset.byUri[uri] = asset; 227 228 return asset; 229 } 230 231 // @needsAudit 232 /** 233 * Downloads the asset data to a local file in the device's cache directory. Once the returned 234 * promise is fulfilled without error, the [`localUri`](#assetlocaluri) field of this asset points 235 * to a local file containing the asset data. The asset is only downloaded if an up-to-date local 236 * file for the asset isn't already present due to an earlier download. The downloaded `Asset` 237 * will be returned when the promise is resolved. 238 * @return Returns a Promise which fulfills with an `Asset` instance. 239 */ 240 async downloadAsync(): Promise<this> { 241 if (this.downloaded) { 242 return this; 243 } 244 if (this.downloading) { 245 await new Promise<void>((resolve, reject) => { 246 this._downloadCallbacks.push({ resolve, reject }); 247 }); 248 return this; 249 } 250 this.downloading = true; 251 252 try { 253 if (Platform.OS === 'web') { 254 if (ImageAssets.isImageType(this.type)) { 255 const { width, height, name } = await ImageAssets.getImageInfoAsync(this.uri); 256 this.width = width; 257 this.height = height; 258 this.name = name; 259 } else { 260 this.name = AssetUris.getFilename(this.uri); 261 } 262 } 263 this.localUri = await downloadAsync(this.uri, this.hash, this.type, this.name); 264 265 this.downloaded = true; 266 this._downloadCallbacks.forEach(({ resolve }) => resolve()); 267 } catch (e) { 268 this._downloadCallbacks.forEach(({ reject }) => reject(e)); 269 throw e; 270 } finally { 271 this.downloading = false; 272 this._downloadCallbacks = []; 273 } 274 return this; 275 } 276} 277