Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,7 @@ This repository holds shell scripts that Intersect uses to engage in Cardano on-
- Checks if a file is accessible via free IPFS gateways
- [ipfs-pin.sh](./scripts/ipfs-pin.sh)
- Pins file(s) across local IPFS node, Pinata, Blockfrost, and NMKR (enabled by default; disable individually with `--no-local`, `--no-pinata`, `--no-blockfrost`, `--no-nmkr`)
- Accepts a single file or a directory; use `--just-jsonld` to limit a directory walk to `.jsonld` files
- `--check-too` first runs `ipfs-check.sh` and skips pinning if the file is already discoverable
- Accepts a single file by default. To recursively pin a directory you must explicitly pass `--directory` (think of it as the equivalent of `rm -r`); without it, a directory path is rejected to prevent accidental bulk uploads. Combine with `--just-jsonld` to limit the walk to `.jsonld` files. The walk skips `.git`/`.svn`/`.hg`, symlinks, and any file whose name matches a sensitive pattern (`*.skey`, `*.vkey`, `.env*`, `id_rsa*`, `*.pem`, `*.p12`, `*.pfx`).

#### CIP-108 Scripts

Expand Down
139 changes: 94 additions & 45 deletions scripts/ipfs-pin.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@

# Can change if you want!

# Default behavior is to not check if file is discoverable on IPFS
CHECK_TOO="false"
JUST_JSONLD="false"

# Pinning services to host the file on IPFS
Expand Down Expand Up @@ -42,15 +40,40 @@ if ! command -v ipfs >/dev/null 2>&1; then
exit 1
fi

# Auth-header tmp file plumbing. We pass the API key to curl via `-H @file`
# (header-file syntax, curl >= 7.55) so the secret never appears in this
# process's argv, where it would otherwise be visible to any user on the
# system via `ps`. Files are mktemp'd with mode 600 and removed on exit.
SECRET_TMP_FILES=()
cleanup_secret_files() {
local f
for f in "${SECRET_TMP_FILES[@]:-}"; do
[ -n "$f" ] && [ -f "$f" ] && rm -f "$f"
done
}
trap cleanup_secret_files EXIT INT TERM

# Create a 0600 tmp file containing one HTTP header line and emit its path on
# stdout. The caller passes that path to `curl -H @path`. Track it for cleanup.
write_auth_header_file() {
local header_line="$1"
local f
f=$(mktemp "${TMPDIR:-/tmp}/ipfs-pin-auth.XXXXXX")
chmod 600 "$f"
printf '%s\n' "$header_line" > "$f"
SECRET_TMP_FILES+=("$f")
printf '%s' "$f"
}

# Usage message
usage() {
local col=50
echo -e "${UNDERLINE}${BOLD}Pin files to local IPFS node and via Blockfrost, NMKR and Pinata${NC}"
echo -e "\n"
echo -e "Syntax:${BOLD} $0 ${GREEN}<file|directory>${NC} [${GREEN}--check-too${NC}] [${GREEN}--no-local${NC}] [${GREEN}--no-pinata${NC}] [${GREEN}--no-blockfrost${NC}] [${GREEN}--no-nmkr${NC}]"
echo -e "Syntax:${BOLD} $0 ${GREEN}<file|directory>${NC} [${GREEN}--directory${NC}] [${GREEN}--no-local${NC}] [${GREEN}--no-pinata${NC}] [${GREEN}--no-blockfrost${NC}] [${GREEN}--no-nmkr${NC}]"
printf "Params: ${GREEN}%-*s${GRAY}%s${NC}\n" $((col-8)) "<file|directory>" "- Path to your file or directory containing files"
printf " ${GREEN}%-*s${NC}${GRAY}%s${NC}\n" $((col-8)) "[--just-jsonld]" "- If a directory is provided, only .jsonld files will be processed (default: $JUST_JSONLD)"
printf " ${GREEN}%-*s${NC}${GRAY}%s${NC}\n" $((col-8)) "[--check-too]" "- Run a check if file is discoverable on ipfs, only pin if not discoverable (default: $CHECK_TOO)"
printf " ${GREEN}%-*s${NC}${GRAY}%s${NC}\n" $((col-8)) "[--directory]" "- REQUIRED if the path is a directory. Without this flag, a directory path is rejected to prevent accidental bulk uploads (e.g. pointing at a project root and pinning every file in it). Treat this as the equivalent of rm's '-r'."
printf " ${GREEN}%-*s${NC}${GRAY}%s${NC}\n" $((col-8)) "[--just-jsonld]" "- If --directory is set, only .jsonld files will be processed (default: $JUST_JSONLD)"
printf " ${GREEN}%-*s${NC}${GRAY}%s${NC}\n" $((col-8)) "[--no-local]" "- Don't try to pin file on local ipfs node (default: $DEFAULT_HOST_ON_LOCAL_NODE)"
printf " ${GREEN}%-*s${NC}${GRAY}%s${NC}\n" $((col-8)) "[--no-pinata]" "- Don't try to pin file on pinata service (default: $DEFAULT_HOST_ON_PINATA)"
printf " ${GREEN}%-*s${NC}${GRAY}%s${NC}\n" $((col-8)) "[--no-blockfrost]" "- Don't try to pin file on blockfrost service (default: $DEFAULT_HOST_ON_BLOCKFROST)"
Expand All @@ -61,8 +84,8 @@ usage() {

# Initialize variables with defaults
input_path=""
check_discoverable="$CHECK_TOO"
just_jsonld="$JUST_JSONLD"
allow_directory="false"
local_host="$DEFAULT_HOST_ON_LOCAL_NODE"
pinata_host="$DEFAULT_HOST_ON_PINATA"
blockfrost_host="$DEFAULT_HOST_ON_BLOCKFROST"
Expand All @@ -71,8 +94,8 @@ nmkr_host="$DEFAULT_HOST_ON_NMKR"
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
--check-too)
check_discoverable="true"
--directory)
allow_directory="true"
shift
;;
--just-jsonld)
Expand Down Expand Up @@ -151,37 +174,39 @@ if [ "$nmkr_host" = "true" ]; then
fi
fi

# Refuse to pin obviously-secret files. Public-IPFS pinning a Cardano signing
# key (.skey) or an .env credentials file would publish it permanently
SENSITIVE_BASENAME_REGEX='\.(skey|vkey|env)$|^\.env(\..*)?$|^id_(rsa|ed25519|ecdsa|dsa)(\.pub)?$|\.(pem|p12|pfx)$'
is_sensitive_filename() {
local name
name=$(basename "$1")
[[ "$name" =~ $SENSITIVE_BASENAME_REGEX ]]
}

# Function to pin a single file
pin_single_file() {
local file="$1"


if is_sensitive_filename "$file"; then
echo -e " "
echo -e "${RED}Refusing to pin '${YELLOW}$file${RED}': filename matches a sensitive pattern (signing key / env / private key / cert). Pinning would publish it permanently to public IPFS.${NC}" >&2
echo -e "${GRAY}If you genuinely intend to pin this file, rename it first or remove it from the directory tree.${NC}" >&2
return 1
fi

echo -e " "
echo -e "${CYAN}Processing file: ${YELLOW}$file${NC}"

# Generate CID from the given file
echo -e "${CYAN}Generating CID for the file...${NC}"

# use ipfs add to generate a CID
# use CIDv1
ipfs_cid=$(ipfs add -Q --cid-version 1 "$file")
echo -e "CID: ${YELLOW}$ipfs_cid${NC}"

# If user wants to check if file is discoverable on IPFS
if [ "$check_discoverable" = "true" ]; then
echo -e "${CYAN}Using ./scripts/ipfs-check.sh script to check if file is discoverable on IPFS...${NC}"
# check if file is discoverable on IPFS
if ! ./scripts/ipfs-check.sh "$file"; then
echo -e "${YELLOW}File is not discoverable on IPFS. Proceeding to pin it.${NC}"
else
echo -e "${GREEN}File is already discoverable on IPFS. No need to pin it.${NC}"
return 0
fi
else
echo -e "${CYAN}Skipping check of file on ipfs...${NC}"
fi


echo -e " "
echo -e "${CYAN}File is not hosted on IPFS, so pinning it...${NC}"
echo -e "${CYAN}Pinning file to enabled services...${NC}"

# Pin on local node
if [ "$local_host" = "true" ]; then
Expand Down Expand Up @@ -210,8 +235,9 @@ pin_single_file() {
fi

echo -e "${CYAN}Uploading file to Pinata service...${NC}"
pinata_auth_file=$(write_auth_header_file "Authorization: Bearer ${PINATA_API_KEY}")
response=$(curl -s -X POST "https://uploads.pinata.cloud/v3/files" \
-H "Authorization: Bearer ${PINATA_API_KEY}" \
-H "@${pinata_auth_file}" \
-F "file=@$file" \
-F "network=public" \
)
Expand Down Expand Up @@ -240,8 +266,9 @@ pin_single_file() {
fi

echo -e "${CYAN}Uploading file to Blockfrost service...${NC}"
blockfrost_auth_file=$(write_auth_header_file "project_id: $BLOCKFROST_API_KEY")
response=$(curl -s -X POST "https://ipfs.blockfrost.io/api/v0/ipfs/add" \
-H "project_id: $BLOCKFROST_API_KEY" \
-H "@${blockfrost_auth_file}" \
-F "file=@$file" \
)
# Check response for errors
Expand Down Expand Up @@ -278,18 +305,18 @@ pin_single_file() {
base64_content=$(base64 -i "$file")

echo -e "${CYAN}Uploading file to NMKR service...${NC}"
nmkr_auth_file=$(write_auth_header_file "Authorization: Bearer ${NMKR_API_KEY}")
# Build the JSON body with jq so the basename and base64 payload are
# properly escaped
nmkr_body=$(jq -n \
--arg b64 "$base64_content" \
--arg name "$(basename "$file")" \
'{fileFromBase64: $b64, name: $name, mimetype: "application/json"}')
response=$(curl -s -X POST "https://studio-api.nmkr.io/v2/UploadToIpfs/${NMKR_USER_ID}" \
-H 'accept: text/plain' \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer ${NMKR_API_KEY}" \
-d @- <<EOF
{
"fileFromBase64": "$base64_content",
"name": "$(basename "$file")",
"mimetype": "application/json"
}
EOF
)
-H "@${nmkr_auth_file}" \
--data-binary @- <<<"$nmkr_body")
# Check response for errors
if echo "$response" | grep -q '"errors":'; then
echo -e "${RED}Error in NMKR response:${NC}" >&2
Expand All @@ -309,22 +336,36 @@ EOF

# Main processing logic
if [ -d "$input_path" ]; then
# If input is a directory: pin files (optionally only .jsonld files) including subdirectories
# Directory uploads must be opted into explicitly
if [ "$allow_directory" != "true" ]; then
echo -e "${RED}Error: '${YELLOW}$input_path${RED}' is a directory.${NC}" >&2
echo -e "${YELLOW}Pass ${GREEN}--directory${YELLOW} to confirm you want to recursively pin its contents (think of this as the equivalent of ${GREEN}rm -r${YELLOW} — it will publish every regular non-symlink, non-VCS, non-sensitive file under the tree to public IPFS, irreversibly).${NC}" >&2
echo -e "${GRAY}Combine with ${GREEN}--just-jsonld${GRAY} to limit the walk to *.jsonld files.${NC}" >&2
exit 1
fi
echo -e " "
echo -e "${YELLOW}Warning: ${GREEN}--directory${YELLOW} is set — this will recursively pin files under '${BRIGHTWHITE}$input_path${YELLOW}' to every enabled pinning service. Pinning is publishing: anything uploaded becomes permanently retrievable from public IPFS gateways.${NC}" >&2
echo -e "${CYAN}Processing directory: ${YELLOW}$input_path${NC}"

# if just jsonld is true, only process .jsonld files

# Pruning rules for the recursive walk:
# - Skip .git / .svn / .hg metadata directories
# - Skip symlinks (` ! -type l`)
PRUNE_EXPR=(
\( -type d \( -name .git -o -name .svn -o -name .hg \) \) -prune
-o
)

files_to_process=()
if [ "$just_jsonld" = "true" ]; then
# Only .jsonld files
# Only .jsonld files (still skipping VCS dirs and symlinks)
while IFS= read -r -d '' file; do
files_to_process+=("$file")
done < <(find "$input_path" -type f -name "*.jsonld" -print0)
done < <(find "$input_path" "${PRUNE_EXPR[@]}" -type f ! -type l -name "*.jsonld" -print0)
else
# else do all files
# All files (still skipping VCS dirs and symlinks)
while IFS= read -r -d '' file; do
files_to_process+=("$file")
done < <(find "$input_path" -type f -print0)
done < <(find "$input_path" "${PRUNE_EXPR[@]}" -type f ! -type l -print0)
fi

# check if any files were found
Expand Down Expand Up @@ -360,7 +401,15 @@ if [ -d "$input_path" ]; then
echo -e "${GREEN}All files processed successfully!${NC}"

elif [ -f "$input_path" ]; then
# Input is a single file
if [ "$allow_directory" = "true" ]; then
echo -e "${RED}Error: ${GREEN}--directory${RED} was set, but '${YELLOW}$input_path${RED}' is a single file. Drop ${GREEN}--directory${RED} for single-file uploads.${NC}" >&2
exit 1
fi
# Input is a single file. Reject symlinks: `[ -f X ]` follows the link.
if [ -L "$input_path" ]; then
echo -e "${RED}Error: '${YELLOW}$input_path${RED}' is a symbolic link. Refusing to pin a symlink target — pass the real file path instead.${NC}" >&2
exit 1
fi
echo -e " "
echo -e "${CYAN}Processing single file: ${YELLOW}$input_path${NC}"
pin_single_file "$input_path"
Expand Down