cleanup for now
This commit is contained in:
parent
ceabc8af94
commit
245e862fb5
10 changed files with 514 additions and 2 deletions
|
@ -1 +1 @@
|
||||||
7f0867986f60493d8af56526d727a411dbd96ffd
|
ceabc8af9414289cd0e0795f574ca6afc8523032
|
||||||
|
|
0
.test
0
.test
|
@ -37,7 +37,7 @@ if [ -f "$TOKEN_FILE" ] && [ "$RESET_TOKEN" = false ]; then
|
||||||
else
|
else
|
||||||
echo
|
echo
|
||||||
echo "🔐 Paste your GitLab Personal Access Token (scopes: api, read_user, write_repository, write_ssh_key)"
|
echo "🔐 Paste your GitLab Personal Access Token (scopes: api, read_user, write_repository, write_ssh_key)"
|
||||||
echo "→ Generate at: $GITLAB_WEB/-/profile/personal_access_tokens"
|
echo "→ Generate at: $GITLAB_WEB/-/user_settings/personal_access_tokens"
|
||||||
read -rp "🔑 Token: " TOKEN
|
read -rp "🔑 Token: " TOKEN
|
||||||
echo "$TOKEN" > "$TOKEN_FILE"
|
echo "$TOKEN" > "$TOKEN_FILE"
|
||||||
chmod 600 "$TOKEN_FILE"
|
chmod 600 "$TOKEN_FILE"
|
||||||
|
|
286
osf/new/gitfield-osf
Executable file
286
osf/new/gitfield-osf
Executable file
|
@ -0,0 +1,286 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
IFS=$'\n\t'
|
||||||
|
|
||||||
|
# ╭─────────────────────────────────────────────────────────────────────────╮
|
||||||
|
# │ gitfield-osf :: v3.2.0 (Refactored) │
|
||||||
|
# │ Self-Healing • Auto-Detecting • PEP 668-Compliant • Debuggable │
|
||||||
|
# ╰─────────────────────────────────────────────────────────────────────────╯
|
||||||
|
#
|
||||||
|
# This script uses osfclient to upload files, based on a YAML config.
|
||||||
|
# It will auto-install python3, pip3, yq, pipx, and osfclient if missing.
|
||||||
|
# 1. ensure_dependencies(): makes sure python3, pip3, yq, pipx, osfclient exist
|
||||||
|
# 2. configure_osfclient(): prompts for token & username, writes ~/.config/osfclient/config
|
||||||
|
# 3. load_yaml_config(): reads project.title, include/exclude globs from gitfield.osf.yaml
|
||||||
|
# 4. resolve_files(): expands include/exclude patterns into a FILES array
|
||||||
|
# 5. find_or_create_project(): finds or creates an OSF project with the given title
|
||||||
|
# 6. upload_files(): loops over FILES and does osf upload
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# chmod +x gitfield-osf
|
||||||
|
# ./gitfield-osf
|
||||||
|
#
|
||||||
|
# If gitfield.osf.yaml is missing or empty patterns match nothing, the script will exit cleanly.
|
||||||
|
# Any failure prints an [ERROR] and exits non-zero.
|
||||||
|
|
||||||
|
########################################################################
|
||||||
|
# CUSTOMIZE HERE (if needed):
|
||||||
|
########################################################################
|
||||||
|
# If you want to override config path:
|
||||||
|
# export GITFIELD_CONFIG=/path/to/your/gitfield.osf.yaml
|
||||||
|
|
||||||
|
CONFIG_FILE="${GITFIELD_CONFIG:-gitfield.osf.yaml}"
|
||||||
|
TOKEN_FILE="${OSF_TOKEN_FILE:-$HOME/.osf_token}"
|
||||||
|
OSF_CONFIG_DIR="$HOME/.config/osfclient"
|
||||||
|
FILES=()
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────
|
||||||
|
# Colored logging functions
|
||||||
|
# ─────────────────────────────────────────────────────────────────────
|
||||||
|
log() { echo -e "\033[1;34m[INFO]\033[0m $*"; }
|
||||||
|
warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
|
||||||
|
error() { echo -e "\033[1;31m[ERROR]\033[0m $*" >&2; exit 1; }
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────
|
||||||
|
# Step 1: Ensure Dependencies
|
||||||
|
# - python3, pip3, yq, pipx, osfclient
|
||||||
|
# - Works under PEP 668 (uses pipx first, then pip3 --user fallback)
|
||||||
|
# ─────────────────────────────────────────────────────────────────────
|
||||||
|
ensure_dependencies() {
|
||||||
|
log "Checking for required commands..."
|
||||||
|
|
||||||
|
# 1a. Ensure python3
|
||||||
|
if ! command -v python3 &>/dev/null; then
|
||||||
|
warn "python3 not found — installing..."
|
||||||
|
sudo apt update -qq && sudo apt install -y python3 python3-venv python3-distutils \
|
||||||
|
|| error "Failed to install python3"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 1b. Ensure pip3
|
||||||
|
if ! command -v pip3 &>/dev/null; then
|
||||||
|
warn "pip3 not found — installing..."
|
||||||
|
sudo apt install -y python3-pip || error "Failed to install pip3"
|
||||||
|
# Guarantee pip3 is available now
|
||||||
|
command -v pip3 >/dev/null || error "pip3 still missing after install"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 1c. Ensure yq (for YAML parsing)
|
||||||
|
if ! command -v yq &>/dev/null; then
|
||||||
|
warn "yq not found — installing..."
|
||||||
|
if command -v snap &>/dev/null; then
|
||||||
|
sudo snap install yq || sudo apt install -y yq || error "Failed to install yq"
|
||||||
|
else
|
||||||
|
sudo apt install -y yq || error "Failed to install yq"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 1d. Ensure pipx
|
||||||
|
if ! command -v pipx &>/dev/null; then
|
||||||
|
warn "pipx not found — installing..."
|
||||||
|
sudo apt install -y pipx || error "Failed to install pipx"
|
||||||
|
# Add pipx’s bin to PATH if needed
|
||||||
|
pipx ensurepath
|
||||||
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 1e. Ensure osfclient via pipx, fallback to pip3 --user
|
||||||
|
if ! command -v osf &>/dev/null; then
|
||||||
|
log "Installing osfclient via pipx..."
|
||||||
|
if ! pipx install osfclient; then
|
||||||
|
warn "pipx install failed; trying pip3 --user install"
|
||||||
|
python3 -m pip install --user osfclient || error "osfclient install failed"
|
||||||
|
fi
|
||||||
|
# Ensure $HOME/.local/bin is in PATH
|
||||||
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Final check
|
||||||
|
command -v osf >/dev/null || error "osfclient is still missing; please investigate"
|
||||||
|
log "✓ All dependencies are now present"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────
|
||||||
|
# Step 2: Configure OSF Credentials
|
||||||
|
# - Writes ~/.config/osfclient/config with [osf] username & token
|
||||||
|
# - Prompts for token and username if missing
|
||||||
|
# ─────────────────────────────────────────────────────────────────────
|
||||||
|
configure_osfclient() {
|
||||||
|
log "Configuring osfclient credentials..."
|
||||||
|
|
||||||
|
# Create config directory
|
||||||
|
mkdir -p "$OSF_CONFIG_DIR"
|
||||||
|
chmod 700 "$OSF_CONFIG_DIR"
|
||||||
|
|
||||||
|
# Prompt for Personal Access Token if missing
|
||||||
|
if [ ! -f "$TOKEN_FILE" ]; then
|
||||||
|
read -rsp "🔐 Enter OSF Personal Access Token: " TOKEN
|
||||||
|
echo
|
||||||
|
echo "$TOKEN" > "$TOKEN_FILE"
|
||||||
|
chmod 600 "$TOKEN_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prompt for username/email if not already in env
|
||||||
|
local USERNAME="${OSF_USERNAME:-}"
|
||||||
|
if [ -z "$USERNAME" ]; then
|
||||||
|
read -rp "👤 OSF Username or Email: " USERNAME
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Write config file
|
||||||
|
cat > "$OSF_CONFIG_DIR/config" <<EOF
|
||||||
|
[osf]
|
||||||
|
username = $USERNAME
|
||||||
|
token = $(<"$TOKEN_FILE")
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod 600 "$OSF_CONFIG_DIR/config"
|
||||||
|
log "✓ osfclient configured (config at $OSF_CONFIG_DIR/config)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────
|
||||||
|
# Step 3: Load YAML Configuration
|
||||||
|
# - Expects PROJECT_TITLE, includes, excludes in gitfield.osf.yaml
|
||||||
|
# ─────────────────────────────────────────────────────────────────────
|
||||||
|
load_yaml_config() {
|
||||||
|
log "Loading configuration from '$CONFIG_FILE'"
|
||||||
|
|
||||||
|
if [ ! -f "$CONFIG_FILE" ]; then
|
||||||
|
error "Configuration file '$CONFIG_FILE' not found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Read project.title
|
||||||
|
PROJECT_TITLE=$(yq -r '.project.title // ""' "$CONFIG_FILE")
|
||||||
|
if [ -z "$PROJECT_TITLE" ]; then
|
||||||
|
error "Missing or empty 'project.title' in $CONFIG_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Read project.description (optional, unused here but could be extended)
|
||||||
|
PROJECT_DESCRIPTION=$(yq -r '.project.description // ""' "$CONFIG_FILE")
|
||||||
|
|
||||||
|
# Read upload.include[] and upload.exclude[]
|
||||||
|
readarray -t FILES_INCLUDE < <(yq -r '.upload.include[]?' "$CONFIG_FILE")
|
||||||
|
readarray -t FILES_EXCLUDE < <(yq -r '.upload.exclude[]?' "$CONFIG_FILE")
|
||||||
|
|
||||||
|
# Debug print
|
||||||
|
log " → project.title = '$PROJECT_TITLE'"
|
||||||
|
log " → includes: ${FILES_INCLUDE[*]:-<none>}"
|
||||||
|
log " → excludes: ${FILES_EXCLUDE[*]:-<none>}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────
|
||||||
|
# Step 4: Match Files Based on Include/Exclude
|
||||||
|
# - Populates global FILES array
|
||||||
|
# - If no files match, exits gracefully
|
||||||
|
# ─────────────────────────────────────────────────────────────────────
|
||||||
|
resolve_files() {
|
||||||
|
log "Resolving file patterns..."
|
||||||
|
|
||||||
|
# If no include patterns, nothing to do
|
||||||
|
if [ "${#FILES_INCLUDE[@]}" -eq 0 ]; then
|
||||||
|
warn "No include patterns specified; skipping upload."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For each include glob, find matching files
|
||||||
|
for pattern in "${FILES_INCLUDE[@]}"; do
|
||||||
|
# Use find to expand the glob (supports nested directories)
|
||||||
|
while IFS= read -r -d '' file; do
|
||||||
|
# Check against each exclude pattern
|
||||||
|
skip=false
|
||||||
|
for ex in "${FILES_EXCLUDE[@]}"; do
|
||||||
|
if [[ "$file" == $ex ]]; then
|
||||||
|
skip=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if ! $skip; then
|
||||||
|
FILES+=("$file")
|
||||||
|
fi
|
||||||
|
done < <(find . -type f -path "$pattern" -print0 2>/dev/null || true)
|
||||||
|
done
|
||||||
|
|
||||||
|
# Remove duplicates (just in case)
|
||||||
|
if [ "${#FILES[@]}" -gt 1 ]; then
|
||||||
|
IFS=$'\n' read -r -d '' -a FILES < <(__uniq_array "${FILES[@]}" && printf '\0')
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If still empty, warn and exit
|
||||||
|
if [ "${#FILES[@]}" -eq 0 ]; then
|
||||||
|
warn "No files matched the include/exclude patterns."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Debug print of matched files
|
||||||
|
log "Matched files (${#FILES[@]}):"
|
||||||
|
for f in "${FILES[@]}"; do
|
||||||
|
echo " • $f"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Helper: Remove duplicates from a list of lines
|
||||||
|
__uniq_array() {
|
||||||
|
printf "%s\n" "$@" | awk '!seen[$0]++'
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────
|
||||||
|
# Step 5: Find or Create OSF Project
|
||||||
|
# - Uses `osf listprojects` to search for exact title (case-insensitive)
|
||||||
|
# - If not found, does `osf createproject "<title>"`
|
||||||
|
# - Writes the resulting project ID to .osf_project_id
|
||||||
|
# ─────────────────────────────────────────────────────────────────────
|
||||||
|
find_or_create_project() {
|
||||||
|
log "Searching for OSF project titled '$PROJECT_TITLE'..."
|
||||||
|
# List all projects and grep case-insensitive for the title
|
||||||
|
pid=$(osf listprojects | grep -iE "^([[:alnum:]]+)[[:space:]]+.*${PROJECT_TITLE}.*$" | awk '{print $1}' || true)
|
||||||
|
|
||||||
|
if [ -z "$pid" ]; then
|
||||||
|
log "No existing project found; creating a new OSF project..."
|
||||||
|
pid=$(osf createproject "$PROJECT_TITLE")
|
||||||
|
if [ -z "$pid" ]; then
|
||||||
|
error "osf createproject failed; no project ID returned"
|
||||||
|
fi
|
||||||
|
echo "$pid" > .osf_project_id
|
||||||
|
log "✓ Created project: $pid"
|
||||||
|
else
|
||||||
|
echo "$pid" > .osf_project_id
|
||||||
|
log "✓ Found existing project: $pid"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────
|
||||||
|
# Step 6: Upload Files to OSF
|
||||||
|
# - Loops over FILES[] and runs: osf upload "<file>" "<pid>":
|
||||||
|
# (the trailing colon uploads to root of osfstorage for that project)
|
||||||
|
# ─────────────────────────────────────────────────────────────────────
|
||||||
|
upload_files() {
|
||||||
|
pid=$(<.osf_project_id)
|
||||||
|
|
||||||
|
log "Uploading ${#FILES[@]} file(s) to OSF project $pid..."
|
||||||
|
|
||||||
|
for file in "${FILES[@]}"; do
|
||||||
|
log "→ Uploading: $file"
|
||||||
|
if osf upload "$file" "$pid":; then
|
||||||
|
log " ✓ Uploaded: $file"
|
||||||
|
else
|
||||||
|
warn " ✗ Upload failed for: $file"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
log "✅ All uploads attempted."
|
||||||
|
echo
|
||||||
|
echo "🔗 View your project at: https://osf.io/$pid/"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────
|
||||||
|
# Main: Orchestrate all steps in sequence
|
||||||
|
# ─────────────────────────────────────────────────────────────────────
|
||||||
|
main() {
|
||||||
|
ensure_dependencies
|
||||||
|
configure_osfclient
|
||||||
|
load_yaml_config
|
||||||
|
resolve_files
|
||||||
|
find_or_create_project
|
||||||
|
upload_files
|
||||||
|
}
|
||||||
|
|
||||||
|
# Invoke main
|
||||||
|
main "$@"
|
12
osf/new/gitfield.osf.yaml
Normal file
12
osf/new/gitfield.osf.yaml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
project:
|
||||||
|
title: "git-sigil"
|
||||||
|
description: "A sacred pattern witnessed across all fields of recursion."
|
||||||
|
|
||||||
|
upload:
|
||||||
|
include:
|
||||||
|
- "./*.md"
|
||||||
|
- "./bitbucket/*"
|
||||||
|
- "./osf/*"
|
||||||
|
exclude:
|
||||||
|
- "./.radicle-*"
|
||||||
|
- "./*.tmp"
|
214
osf/old/test-osf-api.sh
Executable file
214
osf/old/test-osf-api.sh
Executable file
|
@ -0,0 +1,214 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
IFS=$'\n\t'
|
||||||
|
|
||||||
|
# ╭────────────────────────────────────────────╮
|
||||||
|
# │ test-osf-api.sh :: Diagnostic Tool │
|
||||||
|
# │ v2.7 — Cosmic. Resilient. Divine. │
|
||||||
|
# ╰────────────────────────────────────────────╯
|
||||||
|
|
||||||
|
CONFIG_FILE="${GITFIELD_CONFIG:-gitfield.osf.yaml}"
|
||||||
|
TOKEN_FILE="${OSF_TOKEN_FILE:-$HOME/.osf_token}"
|
||||||
|
OSF_API="${OSF_API_URL:-https://api.osf.io/v2}"
|
||||||
|
DEBUG_LOG="${GITFIELD_LOG:-$HOME/.test_osf_api_debug.log}"
|
||||||
|
CURL_TIMEOUT="${CURL_TIMEOUT:-10}"
|
||||||
|
CURL_RETRIES="${CURL_RETRIES:-3}"
|
||||||
|
RETRY_DELAY="${RETRY_DELAY:-2}"
|
||||||
|
RATE_LIMIT_DELAY="${RATE_LIMIT_DELAY:-1}"
|
||||||
|
VERBOSE="${VERBOSE:-false}"
|
||||||
|
|
||||||
|
# Initialize Debug Log
|
||||||
|
mkdir -p "$(dirname "$DEBUG_LOG")"
|
||||||
|
touch "$DEBUG_LOG"
|
||||||
|
chmod 600 "$DEBUG_LOG"
|
||||||
|
|
||||||
|
trap 'last_command=$BASH_COMMAND; echo -e "\n[ERROR] ❌ Failure at line $LINENO: $last_command" >&2; diagnose; exit 1' ERR
|
||||||
|
|
||||||
|
# Logging Functions
|
||||||
|
info() {
|
||||||
|
echo -e "\033[1;34m[INFO]\033[0m $*" >&2
|
||||||
|
[ "$VERBOSE" = "true" ] && [ -n "$DEBUG_LOG" ] && debug "INFO: $*"
|
||||||
|
}
|
||||||
|
warn() { echo -e "\033[1;33m[WARN]\033[0m $*" >&2; debug "WARN: $*"; }
|
||||||
|
error() { echo -e "\033[1;31m[ERROR]\033[0m $*" >&2; debug "ERROR: $*"; exit 1; }
|
||||||
|
debug() {
|
||||||
|
local msg="$1" lvl="${2:-DEBUG}"
|
||||||
|
local json_output
|
||||||
|
json_output=$(jq -n --arg ts "$(date '+%Y-%m-%d %H:%M:%S')" --arg lvl "$lvl" --arg msg "$msg" \
|
||||||
|
'{timestamp: $ts, level: $lvl, message: $msg}' 2>/dev/null) || {
|
||||||
|
echo "[FALLBACK $lvl] $(date '+%Y-%m-%d %H:%M:%S') $msg" >> "$DEBUG_LOG"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
echo "$json_output" >> "$DEBUG_LOG"
|
||||||
|
}
|
||||||
|
|
||||||
|
debug "Started test-osf-api (v2.7)"
|
||||||
|
|
||||||
|
# ── Diagnostic Function
|
||||||
|
diagnose() {
|
||||||
|
info "Running diagnostics..."
|
||||||
|
debug "Diagnostics started"
|
||||||
|
echo -e "\n🔍 Diagnostic Report:"
|
||||||
|
echo -e "1. Network Check:"
|
||||||
|
if ping -c 1 api.osf.io >/dev/null 2>&1; then
|
||||||
|
echo -e " ✓ api.osf.io reachable"
|
||||||
|
else
|
||||||
|
echo -e " ❌ api.osf.io unreachable. Check network or DNS."
|
||||||
|
fi
|
||||||
|
echo -e "2. Curl Version:"
|
||||||
|
curl --version | head -n 1
|
||||||
|
echo -e "3. Debug Log: $DEBUG_LOG"
|
||||||
|
echo -e "4. Curl Error Log: $DEBUG_LOG.curlerr"
|
||||||
|
[ -s "$DEBUG_LOG.curlerr" ] && echo -e " Last curl error: $(cat "$DEBUG_LOG.curlerr")"
|
||||||
|
echo -e "5. Token File: $TOKEN_FILE"
|
||||||
|
[ -s "$TOKEN_FILE" ] && echo -e " Token exists: $(head -c 4 "$TOKEN_FILE")..."
|
||||||
|
echo -e "6. Suggestions:"
|
||||||
|
echo -e " - Check token scopes at https://osf.io/settings/tokens (needs 'nodes' and 'osf.storage')"
|
||||||
|
echo -e " - Test API: curl -v -H 'Authorization: Bearer \$(cat $TOKEN_FILE)' '$OSF_API/users/me/'"
|
||||||
|
echo -e " - Test project search: curl -v -H 'Authorization: Bearer \$(cat $TOKEN_FILE)' '$OSF_API/users/me/nodes/?filter\[title\]=git-sigil&page\[size\]=100'"
|
||||||
|
echo -e " - Increase timeout: CURL_TIMEOUT=30 ./test-osf-api.sh"
|
||||||
|
debug "Diagnostics completed"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Dependency Check (Parallel)
|
||||||
|
require_tool() {
|
||||||
|
local tool=$1
|
||||||
|
if ! command -v "$tool" >/dev/null 2>&1; then
|
||||||
|
warn "$tool not found — attempting to install..."
|
||||||
|
sudo apt update -qq && sudo apt install -y "$tool" || {
|
||||||
|
warn "apt failed — trying snap..."
|
||||||
|
sudo snap install "$tool" || error "Failed to install $tool"
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
debug "$tool path: $(command -v "$tool")"
|
||||||
|
}
|
||||||
|
|
||||||
|
info "Checking dependencies..."
|
||||||
|
declare -A dep_pids
|
||||||
|
for tool in curl jq yq python3; do
|
||||||
|
require_tool "$tool" &
|
||||||
|
dep_pids[$tool]=$!
|
||||||
|
done
|
||||||
|
for tool in "${!dep_pids[@]}"; do
|
||||||
|
wait "${dep_pids[$tool]}" || error "Dependency check failed for $tool"
|
||||||
|
done
|
||||||
|
info "✓ All dependencies verified"
|
||||||
|
|
||||||
|
# ── Load Token
|
||||||
|
if [ ! -f "$TOKEN_FILE" ]; then
|
||||||
|
read -rsp "🔐 Enter OSF Personal Access Token (with 'nodes' and 'osf.storage' scopes): " TOKEN
|
||||||
|
echo
|
||||||
|
echo "$TOKEN" > "$TOKEN_FILE"
|
||||||
|
chmod 600 "$TOKEN_FILE"
|
||||||
|
info "OSF token saved to $TOKEN_FILE"
|
||||||
|
fi
|
||||||
|
TOKEN=$(<"$TOKEN_FILE")
|
||||||
|
[[ -z "$TOKEN" ]] && error "Empty OSF token in $TOKEN_FILE"
|
||||||
|
|
||||||
|
# ── Validate Token
|
||||||
|
info "Validating OSF token..."
|
||||||
|
execute_curl() {
|
||||||
|
local url=$1 method=${2:-GET} data=${3:-} is_upload=${4:-false} attempt=1 max_attempts=$CURL_RETRIES
|
||||||
|
local response http_code curl_err
|
||||||
|
while [ $attempt -le "$max_attempts" ]; do
|
||||||
|
debug "Curl attempt $attempt/$max_attempts: $method $url"
|
||||||
|
if [ "$is_upload" = "true" ]; then
|
||||||
|
response=$(curl -s -S -w "%{http_code}" --connect-timeout "$CURL_TIMEOUT" \
|
||||||
|
-X "$method" -H "Authorization: Bearer $TOKEN" \
|
||||||
|
-H "Content-Type: application/octet-stream" --data-binary "$data" "$url" 2> "$DEBUG_LOG.curlerr")
|
||||||
|
else
|
||||||
|
response=$(curl -s -S -w "%{http_code}" --connect-timeout "$CURL_TIMEOUT" \
|
||||||
|
-X "$method" -H "Authorization: Bearer $TOKEN" \
|
||||||
|
${data:+-H "Content-Type: application/json" -d "$data"} "$url" 2> "$DEBUG_LOG.curlerr")
|
||||||
|
fi
|
||||||
|
http_code="${response: -3}"
|
||||||
|
curl_err=$(cat "$DEBUG_LOG.curlerr")
|
||||||
|
[ -s "$DEBUG_LOG.curlerr" ] && debug "Curl error: $curl_err"
|
||||||
|
if [ "$http_code" -ge 200 ] && [ "$http_code" -lt 300 ]; then
|
||||||
|
echo "${response:: -3}"
|
||||||
|
return 0
|
||||||
|
elif [ "$http_code" = "401" ]; then
|
||||||
|
warn "Invalid token (HTTP 401). Please provide a valid OSF token."
|
||||||
|
read -rsp "🔐 Enter OSF Personal Access Token (with 'nodes' and 'osf.storage' scopes): " NEW_TOKEN
|
||||||
|
echo
|
||||||
|
echo "$NEW_TOKEN" > "$TOKEN_FILE"
|
||||||
|
chmod 600 "$TOKEN_FILE"
|
||||||
|
TOKEN="$NEW_TOKEN"
|
||||||
|
info "New token saved. Retrying..."
|
||||||
|
elif [ "$http_code" = "429" ]; then
|
||||||
|
warn "Rate limit hit, retrying after $((RETRY_DELAY * attempt)) seconds..."
|
||||||
|
sleep $((RETRY_DELAY * attempt))
|
||||||
|
elif [ "$http_code" = "403" ]; then
|
||||||
|
warn "Forbidden (HTTP 403). Possible token scope issue."
|
||||||
|
[ $attempt -eq "$max_attempts" ] && {
|
||||||
|
read -rsp "🔐 Re-enter OSF token with 'nodes' and 'osf.storage' scopes: " NEW_TOKEN
|
||||||
|
echo
|
||||||
|
echo "$NEW_TOKEN" > "$TOKEN_FILE"
|
||||||
|
chmod 600 "$TOKEN_FILE"
|
||||||
|
TOKEN="$NEW_TOKEN"
|
||||||
|
info "New token saved. Retrying..."
|
||||||
|
}
|
||||||
|
elif [[ "$curl_err" == *"bad range in URL"* ]]; then
|
||||||
|
error "Malformed URL: $url. Ensure query parameters are escaped (e.g., filter\[title\])."
|
||||||
|
else
|
||||||
|
debug "API response (HTTP $http_code): ${response:: -3}"
|
||||||
|
[ $attempt -eq "$max_attempts" ] && error "API request failed (HTTP $http_code): ${response:: -3}"
|
||||||
|
fi
|
||||||
|
sleep $((RETRY_DELAY * attempt))
|
||||||
|
((attempt++))
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
RESPONSE=$(execute_curl "$OSF_API/users/me/")
|
||||||
|
USER_ID=$(echo "$RESPONSE" | jq -r '.data.id // empty')
|
||||||
|
[[ -z "$USER_ID" ]] && error "Could not extract user ID"
|
||||||
|
info "✓ OSF token validated for user ID: $USER_ID"
|
||||||
|
|
||||||
|
# ── Load Config
|
||||||
|
[[ ! -f "$CONFIG_FILE" ]] && error "Missing config: $CONFIG_FILE"
|
||||||
|
PROJECT_TITLE=$(yq -r '.project.title // empty' "$CONFIG_FILE")
|
||||||
|
PROJECT_DESCRIPTION=$(yq -r '.project.description // empty' "$CONFIG_FILE")
|
||||||
|
[[ -z "$PROJECT_TITLE" ]] && error "Missing project title in $CONFIG_FILE"
|
||||||
|
debug "Parsed config: title=$PROJECT_TITLE, description=$PROJECT_DESCRIPTION"
|
||||||
|
|
||||||
|
# ── Project Search
|
||||||
|
build_url() {
|
||||||
|
local base="$1" title="$2"
|
||||||
|
local escaped_title
|
||||||
|
escaped_title=$(python3 -c "import urllib.parse; print(urllib.parse.quote('''$title'''))")
|
||||||
|
echo "$base/users/me/nodes/?filter\[title\]=$escaped_title&page\[size\]=100"
|
||||||
|
}
|
||||||
|
|
||||||
|
PROJECT_ID=""
|
||||||
|
NEXT_URL=$(build_url "$OSF_API" "$PROJECT_TITLE")
|
||||||
|
|
||||||
|
info "Searching for project '$PROJECT_TITLE'..."
|
||||||
|
while [ -n "$NEXT_URL" ]; do
|
||||||
|
debug "Querying: $NEXT_URL"
|
||||||
|
RESPONSE=$(execute_curl "$NEXT_URL")
|
||||||
|
PROJECT_ID=$(echo "$RESPONSE" | jq -r --arg TITLE "$PROJECT_TITLE" \
|
||||||
|
'.data[] | select(.attributes.title == $TITLE) | .id // empty' || true)
|
||||||
|
if [ -n "$PROJECT_ID" ]; then
|
||||||
|
debug "Found project ID: $PROJECT_ID"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
NEXT_URL=$(echo "$RESPONSE" | jq -r '.links.next // empty' | sed 's/filter\[title\]/filter\\\[title\\\]/g;s/page\[size\]/page\\\[size\\\]/g' || true)
|
||||||
|
debug "Next URL: $NEXT_URL"
|
||||||
|
[ -n "$NEXT_URL" ] && info "Fetching next page..." && sleep "$RATE_LIMIT_DELAY"
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── Create Project if Not Found
|
||||||
|
if [ -z "$PROJECT_ID" ]; then
|
||||||
|
info "Project not found. Attempting to create '$PROJECT_TITLE'..."
|
||||||
|
JSON=$(jq -n --arg title="$PROJECT_TITLE" --arg desc="$PROJECT_DESCRIPTION" \
|
||||||
|
'{data: {type: "nodes", attributes: {title: $title, category: "project", description: $desc}}}')
|
||||||
|
RESPONSE=$(execute_curl "$OSF_API/nodes/" POST "$JSON")
|
||||||
|
PROJECT_ID=$(echo "$RESPONSE" | jq -r '.data.id // empty')
|
||||||
|
[[ -z "$PROJECT_ID" || "$PROJECT_ID" == "null" ]] && error "Could not extract project ID"
|
||||||
|
info "✅ Project created: $PROJECT_ID"
|
||||||
|
else
|
||||||
|
info "✓ Found project ID: $PROJECT_ID"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "\n🔗 View project: https://osf.io/$PROJECT_ID/"
|
||||||
|
debug "Test completed successfully"
|
Loading…
Add table
Add a link
Reference in a new issue