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