├── files ├── one.png └── two.png ├── img ├── metadata-0.PNG ├── metadata-list.PNG └── pinned-list.PNG ├── metadata ├── 0 └── 1 ├── .eslintrc.js ├── LICENSE.md ├── package.json ├── .vscode └── launch.json ├── src ├── utils.js ├── calculate-cids.js ├── upload-folder.js ├── calculate-hashes.js ├── download-cids.js └── upload-files.js ├── .gitignore └── README.md /files/one.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coderrob/pinata-ipfs-scripts-for-nft-projects/HEAD/files/one.png -------------------------------------------------------------------------------- /files/two.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coderrob/pinata-ipfs-scripts-for-nft-projects/HEAD/files/two.png -------------------------------------------------------------------------------- /img/metadata-0.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coderrob/pinata-ipfs-scripts-for-nft-projects/HEAD/img/metadata-0.PNG -------------------------------------------------------------------------------- /img/metadata-list.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coderrob/pinata-ipfs-scripts-for-nft-projects/HEAD/img/metadata-list.PNG -------------------------------------------------------------------------------- /img/pinned-list.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coderrob/pinata-ipfs-scripts-for-nft-projects/HEAD/img/pinned-list.PNG -------------------------------------------------------------------------------- /metadata/1: -------------------------------------------------------------------------------- 1 | { 2 | "name": "#2", 3 | "description": "The number 2.", 4 | "image": "ipfs://QmazpAaWf3Bb4qhSW9PnQXfj2URbQwdNbZvDr77RbwH7xb", 5 | "seller_fee_basis_points": 400, 6 | "fee_recipient": "", 7 | "attributes": [{ "trait_type": "Number", "value": "Two" }] 8 | } 9 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | commonjs: true, 5 | es2021: true, 6 | }, 7 | extends: [ 8 | 'airbnb-base', 9 | ], 10 | parserOptions: { 11 | ecmaVersion: 'latest', 12 | }, 13 | rules: { 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /metadata/0: -------------------------------------------------------------------------------- 1 | { 2 | "name": "#1", 3 | "description": "The number 1.", 4 | "image": "ipfs://QmZPnX4481toHABEtvKFoCWoVuzFFQRBiA5QR2Cij9pjon", 5 | "seller_fee_basis_points": 400, 6 | "fee_recipient": "", 7 | "attributes": [ 8 | { 9 | "trait_type": "Number", 10 | "value": "One" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Rob (Coderrob) Lindley 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nft-pinata-bulk-upload", 3 | "version": "1.0.0", 4 | "description": "Scripts to upload NFT data to Pinata", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/Coderrob/nft-pinata-bulk-upload.git" 12 | }, 13 | "keywords": [ 14 | "nft", 15 | "pinata", 16 | "metadata" 17 | ], 18 | "author": "coderrob", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/Coderrob/nft-pinata-bulk-upload/issues" 22 | }, 23 | "homepage": "https://github.com/Coderrob/nft-pinata-bulk-upload#readme", 24 | "dependencies": { 25 | "@pinata/sdk": "^1.1.23", 26 | "axios": "^0.30.2", 27 | "base-path-converter": "^1.0.2", 28 | "bottleneck": "^2.19.5", 29 | "dotenv": "^10.0.0", 30 | "form-data": "^4.0.4", 31 | "fs-extra": "^10.0.0", 32 | "ipfs-only-hash": "^4.0.0", 33 | "recursive-fs": "^2.1.0" 34 | }, 35 | "devDependencies": { 36 | "eslint": "^8.10.0", 37 | "eslint-config-airbnb-base": "^15.0.0", 38 | "eslint-plugin-import": "^2.25.4" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "pwa-node", 9 | "request": "launch", 10 | "name": "Download IPFS hash CIDs", 11 | "outputCapture": "std", 12 | "program": "${workspaceFolder}/src/download-cids.js", 13 | "skipFiles": ["/**"], 14 | "envFile": "${workspaceFolder}/.env" 15 | }, 16 | { 17 | "type": "pwa-node", 18 | "request": "launch", 19 | "name": "Calculate File IPFS hash CIDs", 20 | "outputCapture": "std", 21 | "program": "${workspaceFolder}/src/calculate-cids.js", 22 | "skipFiles": ["/**"], 23 | "envFile": "${workspaceFolder}/.env" 24 | }, 25 | { 26 | "type": "pwa-node", 27 | "request": "launch", 28 | "name": "Calculate File sha256 Hashes", 29 | "outputCapture": "std", 30 | "program": "${workspaceFolder}/src/calculate-hashes.js", 31 | "skipFiles": ["/**"], 32 | "envFile": "${workspaceFolder}/.env" 33 | }, 34 | { 35 | "type": "pwa-node", 36 | "request": "launch", 37 | "name": "Upload Files", 38 | "outputCapture": "std", 39 | "program": "${workspaceFolder}/src/upload-files.js", 40 | "skipFiles": ["/**"], 41 | "envFile": "${workspaceFolder}/.env" 42 | }, 43 | { 44 | "type": "pwa-node", 45 | "request": "launch", 46 | "name": "Upload Folder", 47 | "outputCapture": "std", 48 | "program": "${workspaceFolder}/src/upload-folder.js", 49 | "skipFiles": ["/**"], 50 | "envFile": "${workspaceFolder}/.env" 51 | } 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2022 Rob (Coderrob) Lindley 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | 24 | */ 25 | 26 | /** 27 | * Gets the file name from a provided file path. 28 | * @param {string} filePath the file path to extract a file name from 29 | * @return {string} returns the file name from a file path; otherwise an empty string 30 | */ 31 | const getFileName = (filePath) => (filePath && filePath.replace(/^.*[\\/]/, '')) || ''; 32 | 33 | /** 34 | * The possible Pinata file pin statuses 35 | */ 36 | const PinSatus = { 37 | ALL: 'all', // Records for both pinned and unpinned content will be returned 38 | PINNED: 'pinned', // Only records for pinned content will be returned 39 | UNPINNED: 'unpinned', // Only records for unpinned content will be returned 40 | }; 41 | 42 | module.exports = { 43 | getFileName, 44 | PinSatus, 45 | }; 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # Ignore environment files 107 | .env* 108 | 109 | # Ignore script output 110 | output -------------------------------------------------------------------------------- /src/calculate-cids.js: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2022 Rob (Coderrob) Lindley 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | 24 | */ 25 | 26 | const { readFileSync, outputJsonSync } = require('fs-extra'); 27 | const Bottleneck = require('bottleneck'); 28 | const { of } = require('ipfs-only-hash'); 29 | const { read } = require('recursive-fs'); 30 | const { getFileName } = require('./utils'); 31 | 32 | const { log, error } = console; 33 | 34 | (async () => { 35 | const rateLimiter = new Bottleneck({ 36 | maxConcurrent: 5, // arbitrary value - don't overdue file access 37 | }); 38 | 39 | try { 40 | const OUTPUT_PATH = './output/file-cids.json'; 41 | const FOLDER_PATH = 'files'; 42 | const cidMapping = {}; 43 | const { files } = await read(FOLDER_PATH); 44 | if ((files && files.length) <= 0) { 45 | log(`No files were found in folder '${FOLDER_PATH}'`); 46 | return; 47 | } 48 | await Promise.all( 49 | files.map((filePath) => rateLimiter.schedule(async () => { 50 | const fileName = getFileName(filePath); 51 | log(`${fileName} hashing started`); 52 | const fileData = readFileSync(filePath); 53 | const fileHash = await of(fileData); 54 | log(`${fileName} CID: ${fileHash}`); 55 | cidMapping[fileName] = fileHash; 56 | })), 57 | ); 58 | 59 | // Sorting for the resultant object 60 | const sortObject = (obj) => Object.keys(obj) 61 | .sort((a, b) => a.localeCompare(b, 'en', { numeric: true })) 62 | .reduce((accumulator, key) => { 63 | accumulator[key] = obj[key]; 64 | 65 | return accumulator; 66 | }, {}); 67 | 68 | outputJsonSync(OUTPUT_PATH, sortObject(cidMapping)); 69 | } catch (err) { 70 | error(err); 71 | process.exit(1); 72 | } 73 | })(); 74 | -------------------------------------------------------------------------------- /src/upload-folder.js: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2022 Rob (Coderrob) Lindley 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | 24 | */ 25 | 26 | const { post } = require('axios'); 27 | const { createReadStream, outputJsonSync } = require('fs-extra'); 28 | const { read } = require('recursive-fs'); 29 | const FormData = require('form-data'); 30 | const basePathConverter = require('base-path-converter'); 31 | 32 | require('dotenv').config(); 33 | 34 | const { PINATA_API_KEY, PINATA_API_SECRET } = process.env; 35 | 36 | const { log, error } = console; 37 | 38 | const PINATA_API_PINFILETOIPFS = 'https://api.pinata.cloud/pinning/pinFileToIPFS'; 39 | 40 | (async () => { 41 | try { 42 | const OUTPUT_PATH = './output/folder-cid.json'; 43 | const FOLDER_NAME = 'metadata'; // Display name of folder in Pinata 44 | const FOLDER_PATH = 'metadata'; // Folder to be uploaded 45 | const { files } = await read(FOLDER_PATH); 46 | if ((files && files.length) <= 0) { 47 | log(`No files were found in folder '${FOLDER_PATH}'`); 48 | return; 49 | } 50 | log(`'${FOLDER_PATH}' upload started`); 51 | const formData = new FormData(); 52 | files.forEach((filePath) => { 53 | log(`Adding file: ${filePath}`); 54 | formData.append('file', createReadStream(filePath), { 55 | filepath: basePathConverter(FOLDER_PATH, filePath), 56 | }); 57 | }); 58 | formData.append( 59 | 'pinataMetadata', 60 | JSON.stringify({ 61 | name: FOLDER_NAME, 62 | }), 63 | ); 64 | const { 65 | data: { IpfsHash: cid }, 66 | } = await post(PINATA_API_PINFILETOIPFS, formData, { 67 | maxBodyLength: 'Infinity', 68 | headers: { 69 | // eslint-disable-next-line no-underscore-dangle 70 | 'Content-Type': `multipart/form-data; boundary=${formData._boundary}`, 71 | pinata_api_key: PINATA_API_KEY, 72 | pinata_secret_api_key: PINATA_API_SECRET, 73 | }, 74 | }); 75 | log(`'${FOLDER_PATH}' upload complete; CID: ${cid}`); 76 | outputJsonSync(OUTPUT_PATH, { [FOLDER_NAME]: cid }); 77 | } catch (err) { 78 | error(err); 79 | process.exit(1); 80 | } 81 | })(); 82 | -------------------------------------------------------------------------------- /src/calculate-hashes.js: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2022 Rob (Coderrob) Lindley 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | 24 | */ 25 | 26 | const { readFileSync, outputJsonSync } = require('fs-extra'); 27 | const Bottleneck = require('bottleneck'); 28 | const { createHash } = require('crypto'); 29 | const { read } = require('recursive-fs'); 30 | const { getFileName } = require('./utils'); 31 | 32 | const { log, error } = console; 33 | 34 | (async () => { 35 | const rateLimiter = new Bottleneck({ 36 | maxConcurrent: 5, // arbitrary value - don't overdue file access 37 | }); 38 | 39 | try { 40 | const OUTPUT_PATH = './output/file-hashes.json'; 41 | const FINAL_OUTPUT_PATH = './output/file-hashOfHashes.json'; 42 | const FOLDER_PATH = 'files'; 43 | const hashMapping = {}; 44 | const { files } = await read(FOLDER_PATH); 45 | if ((files && files.length) <= 0) { 46 | log(`No files were found in folder '${FOLDER_PATH}'`); 47 | return; 48 | } 49 | await Promise.all( 50 | files.map((filePath) => rateLimiter.schedule(() => { 51 | const fileName = getFileName(filePath); 52 | log(`${fileName} hashing started`); 53 | const fileData = readFileSync(filePath); 54 | const fileHash = createHash('sha256').update(fileData).digest('hex'); 55 | log(`${fileName} SHA-256: ${fileHash}`); 56 | hashMapping[fileName] = fileHash; 57 | })), 58 | ); 59 | 60 | // Sorting for the resultant object 61 | const sortObject = (obj) => Object.keys(obj) 62 | .sort((a, b) => a.localeCompare(b, 'en', { numeric: true })) 63 | .reduce((accumulator, key) => { 64 | accumulator[key] = obj[key]; 65 | 66 | return accumulator; 67 | }, {}); 68 | 69 | outputJsonSync(OUTPUT_PATH, sortObject(hashMapping)); 70 | 71 | // Outputs Hash of hashes 72 | const hashes = require('../output/file-hashes.json'); 73 | const concatenatedStr = Object.values(hashes).join(''); 74 | log('Concatenated String ->', concatenatedStr); 75 | const fileHash = createHash('sha256').update(concatenatedStr).digest('hex'); 76 | log('Final Hash ->', fileHash); 77 | outputJsonSync(FINAL_OUTPUT_PATH, fileHash) 78 | 79 | } catch (err) { 80 | error(err); 81 | process.exit(1); 82 | } 83 | })(); 84 | -------------------------------------------------------------------------------- /src/download-cids.js: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2022 Rob (Coderrob) Lindley 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | 24 | */ 25 | 26 | require('dotenv').config(); 27 | 28 | const { PINATA_API_KEY, PINATA_API_SECRET } = process.env; 29 | const fs = require('fs-extra'); 30 | const pinataSDK = require('@pinata/sdk'); 31 | const { PinSatus } = require('./utils'); 32 | 33 | const { log, table: logTable, error } = console; 34 | 35 | (async () => { 36 | const pinata = pinataSDK(PINATA_API_KEY, PINATA_API_SECRET); 37 | /** 38 | * Get a page of results from Pinata of all pinned files mapped with IPFS CIDs. 39 | * 40 | * @param {number} pageOffset the page index of the results to return. Defaults to 0. 41 | * @param {number} pageLimit the limit to number of results to return. Max of 1000. Default of 5. 42 | * @return {object} returns an object containing file name mapped to its IPFS hash. 43 | * 44 | */ 45 | const getFileCIDMappings = async (status, pageOffset, pageLimit) => { 46 | const filter = { 47 | status, 48 | pageLimit, 49 | pageOffset, 50 | }; 51 | const { count: totalCount, rows } = (await pinata.pinList(filter)) || {}; 52 | const count = (rows && rows.length) || 0; 53 | if (totalCount === 0 || count <= 0) { 54 | if (totalCount === 0) { 55 | log(`No '${status}' files or folders were found`); 56 | } 57 | return { mapping: {}, count }; 58 | } 59 | // Convert array to '[fileName]: CID' property mappings 60 | const mapping = rows.reduce((mappings, row) => { 61 | const { 62 | ipfs_pin_hash: cid, 63 | metadata: { name: fileName }, 64 | } = row; 65 | return { 66 | ...mappings, 67 | ...{ [fileName]: cid }, 68 | }; 69 | }, {}); 70 | return { mapping, count }; 71 | }; 72 | 73 | try { 74 | /** 75 | * The maximum number of Pinata search results supported per page. 76 | */ 77 | const MAX_PAGE_LIMIT = 1000; 78 | /** 79 | * The file pinning status to search for in Pinata. 80 | */ 81 | const PIN_STATUS = PinSatus.ALL; 82 | const OUTPUT_PATH = './output/downloaded-cids.json'; 83 | let totalCount = 0; 84 | let pageOffset = 0; 85 | let cidMappings = {}; 86 | let hasMoreResults = true; 87 | log('Requesting Pinata CID data...'); 88 | while (hasMoreResults) { 89 | // eslint-disable-next-line no-await-in-loop 90 | const { mapping, count } = await getFileCIDMappings(PIN_STATUS, pageOffset, MAX_PAGE_LIMIT); 91 | if (count === 0) { 92 | break; 93 | } 94 | cidMappings = { ...cidMappings, ...mapping }; 95 | hasMoreResults = count >= MAX_PAGE_LIMIT; 96 | pageOffset += count; 97 | totalCount += count; 98 | } 99 | if (totalCount <= 0) { 100 | return; 101 | } 102 | log('Pinata file and folder CIDs:'); 103 | logTable(cidMappings); 104 | fs.outputJsonSync(OUTPUT_PATH, cidMappings); 105 | } catch (err) { 106 | error(err); 107 | process.exit(1); 108 | } 109 | })(); 110 | -------------------------------------------------------------------------------- /src/upload-files.js: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2022 Rob (Coderrob) Lindley 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | 24 | */ 25 | 26 | require('dotenv').config(); 27 | 28 | const { PINATA_API_KEY, PINATA_API_SECRET } = process.env; 29 | const fs = require('fs-extra'); 30 | const recursive = require('recursive-fs'); 31 | const Bottleneck = require('bottleneck'); 32 | const pinataSDK = require('@pinata/sdk'); 33 | const { getFileName } = require('./utils'); 34 | 35 | const { log, error } = console; 36 | 37 | (async () => { 38 | /** 39 | * Load any existing file CID mappings to avoid attempting to upload 40 | * a file that may have already been uploaded and the CID is known. 41 | */ 42 | const pinataCIDs = fs.readJsonSync('./output/downloaded-cids.json') || {}; 43 | const pinata = pinataSDK(PINATA_API_KEY, PINATA_API_SECRET); 44 | 45 | /** 46 | * Set rate limiting close to the maximum of 180 requests / minute. 47 | * These values can be modified to fit your needs while staying 48 | * within the rate limit range to avoid HTTP 429 errors. 49 | * 50 | * Pinata Rate Limit: https://docs.pinata.cloud/rate-limits 51 | */ 52 | const rateLimiter = new Bottleneck({ 53 | maxConcurrent: 1, 54 | minTime: 3000, // Once every 3 seconds 55 | }); 56 | 57 | /** 58 | * Checks whether the provided file name has already been mapped 59 | * with a CID. If so, it does not need to be uploaded again. 60 | * 61 | * @param {string} fileName the file name to check for existing CIDs 62 | * @return {bool} returns true if the file has already been mapped; otherwise false 63 | */ 64 | const cidExists = (fileName) => ({ 65 | exists: !!pinataCIDs[fileName], 66 | ipfsHash: pinataCIDs[fileName], 67 | }); 68 | 69 | /** 70 | * Upload a file's data to Pinata and provide a metadata name for the file. 71 | * This fileName can be either the name of the file being uploaded, or any 72 | * name sufficent enough to identify the contents. 73 | * 74 | * @param {string} fileName the file name to use for the uploaded data 75 | * @param {string} filePath the path to the file to upload and pin to Pinata 76 | * @return {string} returns the IPFS hash (CID) for the uploaded file 77 | */ 78 | const uploadFile = async (fileName, filePath) => { 79 | log(`'${fileName}' upload started`); 80 | const { IpfsHash } = await pinata.pinFileToIPFS( 81 | fs.createReadStream(filePath), 82 | { 83 | pinataMetadata: { 84 | name: fileName, 85 | }, 86 | pinataOptions: { 87 | cidVersion: 0, 88 | }, 89 | }, 90 | ); 91 | log(`'${fileName}' upload complete; CID: ${IpfsHash}`); 92 | return IpfsHash; 93 | }; 94 | 95 | try { 96 | const OUTPUT_PATH = './output/uploaded-cids.json'; 97 | const FOLDER_PATH = 'files'; // Folder containing files to upload 98 | const cidMapping = {}; 99 | const { files } = await recursive.read(FOLDER_PATH); 100 | if ((files && files.length) <= 0) { 101 | log(`No files were found in folder '${FOLDER_PATH}'`); 102 | return; 103 | } 104 | await Promise.all( 105 | files.map(async (filePath) => { 106 | const fileName = getFileName(filePath); 107 | const { exists, ipfsHash } = cidExists(fileName); 108 | if (exists) { 109 | log(`File '${fileName}' already exists; CID: ${ipfsHash}`); 110 | cidMapping[fileName] = ipfsHash; 111 | } else { 112 | cidMapping[fileName] = await rateLimiter.schedule(() => uploadFile(fileName, filePath)); 113 | } 114 | }), 115 | ); 116 | fs.outputJsonSync(OUTPUT_PATH, cidMapping); 117 | } catch (err) { 118 | error(err); 119 | process.exit(1); 120 | } 121 | })(); 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pinata IPFS scripts for NFT projects 2 | 3 | Buy Me A Coffee 4 | 5 | ## Scripts 6 | 7 | The scripts contained in this repository were created to help automate the import and processing of NFT profile pic projects. Each script serves a unique purpose and when combined will help import both NFT images and the associated image metadata. A brief description of the scripts is below: 8 | 9 | - `calculate-cids.js` - calculate the IPFS hash CID for every file in a specified folder 10 | - `calculate-hashes.js` - calculate the sha256 hash for every file in a specified folder 11 | - `download-cids.js` - downloads every pinned file from Pinata for an API account 12 | - `upload-files.js` - uploads the contents of a specified folder and pins each individual file in Pinata 13 | - `upload-folder.js` - uploads the contents of a specified folder and pins the folder container and its contents 14 | 15 | ### Getting Started 16 | 17 | Clone the repository. 18 | 19 | ```bash 20 | git clone https://github.com/Coderrob/pinata-ipfs-scripts-for-nft-projects.git 21 | ``` 22 | 23 | Change directory to the `pinata-ipfs-scripts-for-nft-projects` folder. 24 | 25 | ```bash 26 | cd pinata-ipfs-scripts-for-nft-projects 27 | ``` 28 | 29 | Install dependencies. 30 | 31 | ```bash 32 | npm install 33 | ``` 34 | 35 | Some scripts require environment variables to connect with the [Pinata API](https://docs.pinata.cloud/). These environment variables are needed to download pinned files, or to upload files and folders. 36 | 37 | #### Environment Variables 38 | 39 | `PINATA_API_KEY` - The Pinata API Key environment variable 40 | 41 | `PINATA_API_SECRET` - The Pinata API Secret environment variable 42 | 43 | The repo is setup with [dotenv](https://github.com/motdotla/dotenv) and configured to allow using an `.env` file to run the scripts. 44 | 45 | If the env file does not already exist simply create a new `.env` file at the root of the repository. 46 | 47 | The contents of the `.env` file should look similar to this: 48 | 49 | ```ini 50 | PINATA_API_KEY="a1237a8dcd87766ff4" 51 | PINATA_API_SECRET="fb8654309ca8777asdf7558758123456asdf817166927aknnk888877" 52 | ``` 53 | 54 | To generate these Pinata API keys you'll need to follow the [Getting Started](https://docs.pinata.cloud/#your-api-keys) Pinata documentation 55 | 56 | ### Calculate File IPFS CIDs 57 | 58 | `/src/calculate-cids.js` 59 | 60 | The calculate file CIDs script will iterate the contents of a specified folder, and for each file will compute the IPFS hash CID mapped to the file name. 61 | 62 | Once complete the script will output the file name and CID mappings to a file. 63 | 64 | #### Settings 65 | 66 | `var: OUTPUT_PATH` - The relative output file path. Defaulted to `./output/file-cids.json`. 67 | 68 | `var: FOLDER_PATH` - The relative folder path containing the files to be processed. Each file will have its name and CID mapped. Defaulted to the `files` folder. 69 | 70 | #### Command 71 | 72 | ```bash 73 | node ./src/calculate-cids.js 74 | ``` 75 | 76 | #### Output 77 | 78 | `./output/file-cids.json` 79 | 80 | #### Contents 81 | 82 | ```json 83 | { 84 | "one.png": "QmZPnX4481toHABEtvKFoCWoVuzFFQRBiA5QR2Cij9pjon", 85 | "two.png": "QmazpAaWf3Bb4qhSW9PnQXfj2URbQwdNbZvDr77RbwH7xb" 86 | } 87 | ``` 88 | 89 | ### Calculate File sha256 Hashes 90 | 91 | `/src/calculate-hashes.js` 92 | 93 | The calculate file hashes script will iterate the contents of a specified folder, and for each file will compute the sha256 hash mapped to the file name. 94 | 95 | Once complete the script will output the file name and sha256 hash mappings to a file. 96 | 97 | #### Settings 98 | 99 | `var: OUTPUT_PATH` - The relative output file path. Defaulted to `./output/file-hashes.json`. 100 | 101 | `var: FOLDER_PATH` - The relative folder path containing the files to be processed. Each file will have its name and CIDsha256 hash mapped. Defaulted to the `files` folder. 102 | 103 | #### Command 104 | 105 | ```bash 106 | node ./src/calculate-hashes.js 107 | ``` 108 | 109 | #### Output 110 | 111 | `./output/file-hashes.json` 112 | 113 | #### Contents 114 | 115 | ```json 116 | { 117 | "one.png": "f8e50b5c45e6304b41f87686db539dd52138b873a3af98cc60f623d47a133df2", 118 | "two.png": "76d9c6f8dc113fff71a180195077526fce3d0279034a37f23860c1f519512e94" 119 | } 120 | ``` 121 | 122 | ### Download Pinata Pinned CIDs 123 | 124 | `/src/download-cids.js` 125 | 126 | The download file CIDs script will iterate all pinned files associated with the Pinata API Key. The script will map each row's file name and IPFS hash CID. 127 | 128 | Once complete the script will output the file name and CID mappings to a file. 129 | 130 | #### Settings 131 | 132 | `var: OUTPUT_PATH` - The relative output file path. Defaulted to `./output/downloaded-cids.json` 133 | 134 | `env: PINATA_API_KEY` - The Pinata API Key environment value 135 | 136 | `evn: PINATA_API_SECRET` - The Pinata API Secret environment value 137 | 138 | #### Command 139 | 140 | ```bash 141 | node ./src/download-cids.js 142 | ``` 143 | 144 | #### Output 145 | 146 | `./output/downloaded-cids.json` 147 | 148 | #### Contents 149 | 150 | ```json 151 | { 152 | "one.png": "QmZPnX4481toHABEtvKFoCWoVuzFFQRBiA5QR2Cij9pjon", 153 | "two.png": "QmazpAaWf3Bb4qhSW9PnQXfj2URbQwdNbZvDr77RbwH7xb" 154 | } 155 | ``` 156 | 157 | ### Upload Files 158 | 159 | `/src/upload-files.js` 160 | 161 | The upload files script will iterate the contents of a specified folder and will upload and pin each _individual_ file to Pinata. After a successful upload the file name will be mapped to the IPFS hash CID from the response. 162 | 163 | Once complete the script will output the file name and CID mappings to a file. 164 | 165 | #### Settings 166 | 167 | `var: pinataCIDs` - To prevent re-uploading already pinned files in Pinata. This variable is loaded with the json contents of the `./ouput/downloaded-cids.json` file if one exists. These CID mappings will help prevent re-uploading a file that has already been pinned in Pinata. 168 | 169 | `var: OUTPUT_PATH` - The relative output file path. Defaulted to `./output/uploaded-cids.json`. 170 | 171 | `var: FOLDER_PATH` - The relative folder path to read and upload all local files to be pinned with Pinata. Defaulted to the `files` folder. 172 | 173 | `env: PINATA_API_KEY` - The Pinata API Key environment value 174 | 175 | `env: PINATA_API_SECRET` - The Pinata API Secret environment value 176 | 177 | #### Command 178 | 179 | ```bash 180 | node ./src/upload-files.js 181 | ``` 182 | 183 | #### Output 184 | 185 | `./output/uploaded-cids.json` 186 | 187 | #### Contents 188 | 189 | ```json 190 | { 191 | "one.png": "QmZPnX4481toHABEtvKFoCWoVuzFFQRBiA5QR2Cij9pjon", 192 | "two.png": "QmazpAaWf3Bb4qhSW9PnQXfj2URbQwdNbZvDr77RbwH7xb" 193 | } 194 | ``` 195 | 196 | ### Upload Folder 197 | 198 | `/src/upload-folder.js` 199 | 200 | The upload folder script will iterate the contents of a specified folder and will upload and pin each file under a folder container in Pinata. After a successful upload the folder name will be mapped to the IPFS hash CID from the response. 201 | 202 | Once complete the script will output the folder name and CID mapping to a file. 203 | 204 | > **Note** - To support `ipfs//` such as `ipfs/QmR5m9zJDSmrLnYMawrySYu3wLgN5afo3yizevAaimjvmD/0` simply name the JSON files with numeric names and strip the file extensions. This will allow the files to be accessed by a numeric file name that can be easily mapped to the `TokenId`. 205 | 206 | ![Pinata pinned file list](https://github.com/Coderrob/nft-pinata-bulk-upload/blob/master/img/pinned-list.PNG) 207 | 208 | ![metadata folder container list](https://github.com/Coderrob/nft-pinata-bulk-upload/blob/master/img/metadata-list.PNG) 209 | 210 | ![File 0 metadata json](https://github.com/Coderrob/nft-pinata-bulk-upload/blob/master/img/metadata-0.PNG) 211 | 212 | #### Settings 213 | 214 | `var: OUTPUT_PATH` - The relative output file path. Defaulted to `./output/folder-cid.json`. 215 | 216 | `var: FOLDER_NAME` - The folder name to use for the uploaded folder of json metadata. This can be changed to any name you'd like that identifies the collection of metadata files. Defaulted to `metadata` as the folder name. 217 | 218 | `var: FOLDER_PATH` - The relative folder path to read and upload all local files to be pinned in Pinata as a folder container for the uploaded files. Defaulted to the `metadata` folder. 219 | 220 | `env: PINATA_API_KEY` - The Pinata API Key environment value 221 | 222 | `env: PINATA_API_SECRET` - The Pinata API Secret environment value 223 | 224 | #### Command 225 | 226 | ```bash 227 | node ./src/upload-folder.js 228 | ``` 229 | 230 | #### Output 231 | 232 | `./output/folder-cid.json` 233 | 234 | #### Contents 235 | 236 | ```json 237 | { 238 | "metadata": "QmR5m9zJDSmrLnYMawrySYu3wLgN5afo3yizevAaimjvmD" 239 | } 240 | ``` 241 | --------------------------------------------------------------------------------