@infinixjoyd/metaplex-auth-service
Version:
A client library for nft.storage designed for metaplex NFT uploads
171 lines (170 loc) • 7.55 kB
JavaScript
import { File } from 'nft.storage';
import { fs, path } from '../platform.js';
import { ensureValidMetadata } from '../metadata/index.js';
import { prepareMetaplexNFT } from './prepare.js';
import { isBrowser } from '../utils.js';
/**
* Loads a Metaplex NFT from the filesystem, including metadata json, main image, and any
* additional files referenced in the metadata.
*
* Loads [Metaplex NFT metadata JSON](https://docs.metaplex.com/nft-standard) from `metadataFilePath`,
* using the image located at `imageFilePath`. If `imageFilePath` is not provided, attempts to find the image
* in the following way:
*
* - If the metadata JSON object's `image` field contains the path to a file
* and the file exists, use it
* - Otherwise, take the filename of the metadata file (e.g. `1.json`)
* and look for a file with the same basename and a `.png` extension (e.g. `1.png`).
*
* If no image file can be found, the returned promise will reject with an Error.
*
* In addition to the `image` field, if the `animation_url` contains a valid file path,
* the file will be uploaded to NFT.Storage, and `animation_url` will be set to an
* IPFS HTTP gateway link to the content.
*
* Entries in `properties.files` that contain valid file paths as their `uri` value will also be uploaded to
* NFT.Storage, and each file will have two entries in the final metadata's `properties.files`
* array. One entry contains an HTTP gateway URL as the `uri`, with the `cdn` field set to `true`, while the
* other contains an `ipfs://` URI, with `cdn` set to `false`. This preserves the location-independent
* "canonical" IPFS URI in the blockchain-linked record, while signalling to HTTP-only clients that they
* can use the `cdn` variant.
*
* All file paths contained in the metadata should be relative to the directory containing the metadata file.
*
* Note that this function does NOT store anything with NFT.Storage. To store the returned `PackagedNFT` object,
* see {@link NFTStorageMetaplexor.storePreparedNFT}, or use {@link NFTStorageMetaplexor.storeNFTFromFilesystem},
* which calls this function and stores the result.
*
* This function is only available on node.js and will throw if invoked from a browser runtime.
*
* @param metadataFilePath path to a JSON file containing Metaplex NFT metadata
* @param imageFilePath path to an image to be used as the primary `image` content for the NFT. If not provided,
* the image will be located as described above.
* @param opts
* @param opts.blockstore a Blockstore instance to use when packing objects into CARs. If not provided, a new temporary Blockstore will be created.
* @param opts.validateSchema if true, validate the metadata against a JSON schema before processing. off by default
* @param opts.gatewayHost the hostname of an IPFS HTTP gateway to use in metadata links. Defaults to "nftstorage.link" if not set.
*
* @returns on success, a {@link PackagedNFT} object containing the parsed metadata and the CAR data to upload
* to NFT.Storage.
*/
export async function loadNFTFromFilesystem(metadataFilePath, imageFilePath, opts = {}) {
if (isBrowser) {
throw new Error('loadNFTFromFilesystem is only supported on node.js');
}
const metadataContent = await fs.promises.readFile(metadataFilePath, {
encoding: 'utf-8',
});
const metadataJSON = JSON.parse(metadataContent);
const metadata = opts.validateSchema
? ensureValidMetadata(metadataJSON)
: metadataJSON;
const parentDir = path.dirname(metadataFilePath);
// if no image path was provided, check if metadata.image contains a valid file path
if (!imageFilePath) {
const pathFromMetadata = path.resolve(parentDir, metadata.image);
if (metadata.image && (await fileExists(pathFromMetadata))) {
imageFilePath = pathFromMetadata;
}
else {
// as a last resort, look for a file based on the metadata filename.
// for example, if metadata filename is `0.json`, look for `0.png`.
const basename = path.basename(metadataFilePath, '.json');
const pathFromMetadataFilename = path.resolve(parentDir, basename + '.png');
if (await fileExists(pathFromMetadataFilename)) {
imageFilePath = pathFromMetadataFilename;
}
}
}
// if we still don't have an image file path, bail out
if (!imageFilePath) {
throw new Error(`unable to determine path to image.`);
}
const imageFile = await fileFromPath(imageFilePath, parentDir);
// look for valid file paths in `properties.files`
const additionalFilePaths = new Set();
const properties = metadata.properties || {};
const files = properties.files || [];
for (const f of files) {
const filepath = path.resolve(parentDir, f.uri);
if (await fileExists(filepath)) {
additionalFilePaths.add(filepath);
}
}
// if the image file is also in properties.files (which should be the case),
// remove it from "additional" files to prevent it being processed twice
additionalFilePaths.delete(path.basename(imageFilePath));
// load all discovered files from disk (except image, which we already have)
const additionalFilePromises = [...additionalFilePaths].map((p) => fileFromPath(p, parentDir));
const additionalAssetFiles = await Promise.all(additionalFilePromises);
// package up for storage and return the result
return prepareMetaplexNFT(metadata, imageFile, {
additionalAssetFiles,
blockstore: opts.blockstore,
gatewayHost: opts.gatewayHost,
validateSchema: opts.validateSchema,
});
}
export async function* loadAllNFTsFromDirectory(directoryPath, opts = {}) {
for await (const filename of walk(directoryPath)) {
if (!filename.endsWith('.json')) {
continue;
}
const nft = await loadNFTFromFilesystem(filename, undefined, opts);
yield nft;
}
}
// helpers
/**
* Returns a File object with the contents of the file at the given path.
* The `name` property of the returned file will be relative to the given `rootDir`,
* for example:
*
* ```js
* const f = await fileFromPath('/var/foo/stuff.txt', '/var/foo')
* console.log(f.name) // => 'stuff.txt'
* ```
*
* @param filepath
* @param rootDir
* @returns
*/
async function fileFromPath(filepath, rootDir = '') {
const content = await fs.promises.readFile(filepath);
const filename = path.relative(rootDir, filepath);
return new File([content], filename);
}
/**
*
* @param filepath path to a file whose existence is in doubt
* @returns true if the file exists, false if not
*/
async function fileExists(filepath) {
if (isBrowser) {
return false;
}
try {
await fs.promises.stat(filepath);
return true;
}
catch (e) {
return false;
}
}
async function* walk(dir) {
if (isBrowser) {
return;
}
const files = await fs.promises.readdir(dir);
for (const file of files) {
const stat = await fs.promises.stat(path.join(dir, file));
if (stat.isDirectory()) {
for await (const filename of walk(path.join(dir, file))) {
yield filename;
}
}
else {
yield path.join(dir, file);
}
}
}