From 7aa01fcde2887c2a7cb8a3e15c1221e466c6cccb Mon Sep 17 00:00:00 2001 From: marston Date: Wed, 20 Aug 2025 13:58:36 -0400 Subject: [PATCH 1/4] updating pin to use kubo --- .dockerignore | 61 ++++++++++++-- .env.example | 6 +- DOCKER_README.md | 199 +++++++++++++++++++++++++++++++++++++++++++++ Dockerfile | 56 ++++++++++--- Makefile | 98 ++++++++++++++++++++++ docker-compose.yml | 116 ++++++++++++++++++++++++++ env.template | 30 +++++++ init.sql | 80 ++++++++++++++++++ nginx.conf | 149 +++++++++++++++++++++++++++++++++ 9 files changed, 777 insertions(+), 18 deletions(-) create mode 100644 DOCKER_README.md create mode 100644 docker-compose.yml create mode 100644 env.template create mode 100644 init.sql create mode 100644 nginx.conf diff --git a/.dockerignore b/.dockerignore index 87a2a30..394f3eb 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,7 +1,58 @@ -.env.example +# Git +.git .gitignore +.gitattributes + +# Docker +Dockerfile +docker-compose.yml +.dockerignore + +# Documentation README.md -exec.sh -exec.ps1 -src/ -pkg/ +*.md + +# IDE and editor files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Logs +*.log +logs/ + +# Temporary files +tmp/ +temp/ + +# Build artifacts +bin/ +dist/ +build/ + +# Test files +*_test.go +test/ +tests/ + +# Environment files (keep .env.example if you have one) +.env +.env.local +.env.production + +# Node modules (if any) +node_modules/ + +# Go workspace files +go.work diff --git a/.env.example b/.env.example index 0878fd9..57b07c6 100644 --- a/.env.example +++ b/.env.example @@ -1,10 +1,14 @@ AUTH0_CLIENT_ID={your-id-here} -AUTH0_DOMAIN=deeztop.auth0.com +AUTH0_DOMAIN=jackalpin.ca.auth0.com AUTH0_CLIENT_SECRET={your-secret-here} AUTH0_CALLBACK_URL=http://localhost:3049/callback AUTH0_AUDIENCE= + DATABASE_URL= + SERVER_DOMAIN=http://localhost:3049 + +STRIPE_PUBLIC_KEY= STRIPE_SOCKET_SECRET= JACKAL_SEED={your-seed-here} diff --git a/DOCKER_README.md b/DOCKER_README.md new file mode 100644 index 0000000..0d5b311 --- /dev/null +++ b/DOCKER_README.md @@ -0,0 +1,199 @@ +# IPFSPinner Docker Setup + +This document describes how to run IPFSPinner using Docker and Docker Compose. + +## Prerequisites + +- Docker (version 20.10 or later) +- Docker Compose (version 2.0 or later) +- Make (optional, for using the Makefile commands) + +## Quick Start + +1. **Clone the repository and navigate to the project directory:** + ```bash + cd IPFSPinner + ``` + +2. **Set up environment variables:** + ```bash + cp env.template .env + # Edit .env with your actual values + ``` + +3. **Build and start the services:** + ```bash + make build + make up + ``` + + Or without Make: + ```bash + docker-compose build + docker-compose up -d + ``` + +4. **Check service status:** + ```bash + make status + ``` + +## Services + +The Docker Compose setup includes the following services: + +### 1. PostgreSQL Database (`postgres`) +- **Port:** 5432 +- **Database:** ipfspinner +- **User:** ipfspinner +- **Password:** ipfspinner_password +- **Data Volume:** `postgres_data` + +### 2. IPFS/Kubo Node (`ipfs`) +- **P2P Port:** 4001 +- **API Port:** 5001 +- **Gateway Port:** 8080 +- **Data Volume:** `ipfs_data` +- **Staging Volume:** `ipfs_staging` + +### 3. IPFSPinner Application (`app`) +- **Port:** 3159 +- **Depends on:** postgres, ipfs +- **Uploads Volume:** `./uploads` + +### 4. Nginx Reverse Proxy (`nginx`) - Optional +- **Ports:** 80, 443 +- **Depends on:** app + +## Environment Variables + +Create a `.env` file based on `env.template` with the following required variables: + +- `DATABASE_URL`: PostgreSQL connection string +- `STRIPE_PUBLIC_KEY`: Your Stripe public key +- `JACKAL_SEED`: Your Jackal wallet seed phrase + +## Useful Commands + +### Using Make (recommended) +```bash +make help # Show all available commands +make up # Start services +make down # Stop services +make logs # Show logs +make clean # Clean up everything +make shell-app # Shell into app container +make shell-db # Connect to database +make shell-ipfs # Shell into IPFS container +``` + +### Using Docker Compose directly +```bash +docker-compose up -d # Start services in background +docker-compose down # Stop services +docker-compose logs -f # Follow logs +docker-compose exec app sh # Shell into app container +docker-compose exec postgres psql -U ipfspinner -d ipfspinner # Database shell +``` + +## Development Workflow + +1. **Start services:** + ```bash + make up + ``` + +2. **View logs:** + ```bash + make logs + ``` + +3. **Make code changes and rebuild:** + ```bash + make down + make build + make up + ``` + +4. **Access services:** + - Application: http://localhost:3159 + - IPFS Gateway: http://localhost:8080 + - IPFS API: http://localhost:5001 + - Database: localhost:5432 + +## Database Management + +### Backup +```bash +make backup-db +``` + +### Restore +```bash +make restore-db backup_file=backup_20241201_120000.sql +``` + +### Reset +```bash +make clean +make up +``` + +## IPFS Configuration + +After starting the IPFS container for the first time, you may want to configure CORS headers: + +```bash +make init-ipfs +``` + +This configures the IPFS node to allow cross-origin requests from your application. + +## Troubleshooting + +### Service won't start +1. Check logs: `make logs` +2. Verify environment variables are set correctly +3. Check if ports are already in use +4. Ensure Docker has enough resources allocated + +### Database connection issues +1. Verify PostgreSQL is running: `make status` +2. Check database logs: `make logs-db` +3. Verify DATABASE_URL in .env file + +### IPFS issues +1. Check IPFS logs: `make logs-ipfs` +2. Verify IPFS node is accessible: `curl http://localhost:5001/api/v0/version` +3. Check if IPFS data volume has proper permissions + +### Application issues +1. Check application logs: `make logs-app` +2. Verify all environment variables are set +3. Check if the application can connect to database and IPFS + +## Scaling + +Scale the application service: +```bash +make scale-app scale-app=3 +``` + +## Cleanup + +To completely remove all containers, networks, and volumes: +```bash +make clean +``` + +This will remove all data. Use with caution in production! + +## Production Considerations + +- Change default passwords in docker-compose.yml +- Use external PostgreSQL database for production +- Configure proper SSL certificates for nginx +- Set up monitoring and logging +- Use Docker secrets for sensitive environment variables +- Configure proper backup strategies +- Set up health checks and restart policies \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 6559b50..5ad8fc3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,20 +1,52 @@ -FROM golang:1.19.3-alpine3.17 +# Build stage +FROM golang:1.22-alpine AS builder -# Define current working directory -WORKDIR /01-Login +# Install build dependencies +RUN apk add --no-cache git ca-certificates tzdata -# Download modules to local cache so we can skip re- -# downloading on consecutive docker build commands -COPY go.mod . -COPY go.sum . +# Set working directory +WORKDIR /app + +# Copy go mod files +COPY go.mod go.sum ./ + +# Download dependencies RUN go mod download -# Add sources +# Copy source code COPY . . -RUN go build -o out/auth0-go-web-app . +# Build the application +RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main . + +# Final stage +FROM alpine:latest + +# Install runtime dependencies +RUN apk --no-cache add ca-certificates tzdata + +# Create non-root user +RUN addgroup -g 1001 -S appgroup && \ + adduser -u 1001 -S appuser -G appgroup + +# Set working directory +WORKDIR /app + +# Copy binary from builder stage +COPY --from=builder /app/main . + +# Change ownership to non-root user +RUN chown -R appuser:appgroup /app + +# Switch to non-root user +USER appuser + +# Expose port +EXPOSE 3159 -# Expose port 3000 for our web app binary -EXPOSE 3000 +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:3159/health || exit 1 -CMD ["/01-Login/out/auth0-go-web-app"] +# Run the application +CMD ["./main"] diff --git a/Makefile b/Makefile index f475a36..8d02456 100644 --- a/Makefile +++ b/Makefile @@ -67,3 +67,101 @@ test-unit: @echo "Executing unit tests..." @go test -mod=readonly -v -coverprofile coverage.txt ./... .PHONY: test-unit + +.PHONY: help build up down logs clean restart shell-app shell-db shell-ipfs test + +# Default target +help: + @echo "IPFSPinner Docker Management Commands:" + @echo "" + @echo " build - Build the Docker images" + @echo " up - Start all services" + @echo " down - Stop all services" + @echo " logs - Show logs from all services" + @echo " clean - Remove all containers, networks, and volumes" + @echo " restart - Restart all services" + @echo " shell-app - Open shell in the application container" + @echo " shell-db - Open shell in the PostgreSQL container" + @echo " shell-ipfs - Open shell in the IPFS container" + @echo " test - Run tests in the application container" + +# Build the Docker images +build: + docker-compose build + +# Start all services +up: + docker-compose up -d + +# Start all services and show logs +up-logs: + docker-compose up + +# Stop all services +down: + docker-compose down + +# Show logs from all services +logs: + docker-compose logs -f + +# Show logs from specific service +logs-app: + docker-compose logs -f app + +logs-db: + docker-compose logs -f postgres + +logs-ipfs: + docker-compose logs -f ipfs + +# Remove all containers, networks, and volumes +clean: + docker-compose down -v --remove-orphans + docker system prune -f + +# Restart all services +restart: + docker-compose restart + +# Open shell in the application container +shell-app: + docker-compose exec app sh + +# Open shell in the PostgreSQL container +shell-db: + docker-compose exec postgres psql -U ipfspinner -d ipfspinner + +# Open shell in the IPFS container +shell-ipfs: + docker-compose exec ipfs sh + +# Run tests in the application container +test: + docker-compose exec app go test ./... + +# Check service status +status: + docker-compose ps + +# View service health +health: + docker-compose ps --format "table {{.Name}}\t{{.Status}}\t{{.Ports}}" + +# Scale services (e.g., make scale-app=3) +scale-app: + docker-compose up -d --scale app=$(scale-app) + +# Backup database +backup-db: + docker-compose exec postgres pg_dump -U ipfspinner ipfspinner > backup_$(shell date +%Y%m%d_%H%M%S).sql + +# Restore database from backup +restore-db: + docker-compose exec -T postgres psql -U ipfspinner -d ipfspinner < $(backup_file) + +# Initialize IPFS node (first time setup) +init-ipfs: + docker-compose exec ipfs ipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin '["*"]' + docker-compose exec ipfs ipfs config --json API.HTTPHeaders.Access-Control-Allow-Methods '["PUT", "POST", "GET"]' + docker-compose exec ipfs ipfs config --json API.HTTPHeaders.Access-Control-Allow-Headers '["Authorization"]' diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..55fd400 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,116 @@ +version: '3.8' + +services: + # PostgreSQL Database + postgres: + image: postgres:15-alpine + container_name: ipfspinner-postgres + environment: + POSTGRES_DB: ipfspinner + POSTGRES_USER: ipfspinner + POSTGRES_PASSWORD: ipfspinner_password + POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C" + volumes: + - postgres_data:/var/lib/postgresql/data + - ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro + ports: + - "5432:5432" + networks: + - ipfspinner-network + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ipfspinner -d ipfspinner"] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + + # IPFS/Kubo Node + ipfs: + image: ipfs/kubo:latest + container_name: ipfspinner-ipfs + ports: + - "4001:4001" # P2P port + - "5001:5001" # API port + - "8080:8080" # Gateway port + volumes: + - ipfs_data:/data/ipfs + - ipfs_staging:/export + environment: + - IPFS_PROFILE=server + networks: + - ipfspinner-network + healthcheck: + test: ["CMD", "ipfs", "--api", "/ip4/127.0.0.1/tcp/5001", "dag", "stat", "/ipfs/QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn"] + interval: 30s + timeout: 10s + retries: 3 + restart: unless-stopped + + # IPFSPinner Application + app: + build: + context: . + dockerfile: Dockerfile + container_name: ipfspinner-app + ports: + - "3159:3159" + environment: + - DATABASE_URL=postgres://ipfspinner:ipfspinner_password@postgres:5432/ipfspinner?sslmode=disable + - JACKAL_SEED=${JACKAL_SEED} + - IPFS_API_URL=http://ipfs:5001 + - IPFS_GATEWAY_URL=http://ipfs:8080 + - AUTH0_CLIENT_ID=${AUTH0_CLIENT_ID} + - AUTH0_DOMAIN=${AUTH0_DOMAIN} + - AUTH0_CLIENT_SECRET=${AUTH0_CLIENT_SECRET} + - AUTH0_CALLBACK_URL=${AUTH0_CALLBACK_URL} + - AUTH0_AUDIENCE=${AUTH0_AUDIENCE} + - SERVER_DOMAIN=${SERVER_DOMAIN} + - STRIPE_SOCKET_SECRET=${STRIPE_SOCKET_SECRET} + - STRIPE_PUBLIC_KEY=${STRIPE_PUBLIC_KEY} + + volumes: + - ./uploads:/app/uploads + depends_on: + postgres: + condition: service_healthy + ipfs: + condition: service_healthy + networks: + - ipfspinner-network + restart: unless-stopped + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3159/health"] + interval: 30s + timeout: 10s + retries: 3 + + # Nginx Reverse Proxy (Optional) + nginx: + image: nginx:alpine + container_name: ipfspinner-nginx + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf:ro + - ./ssl:/etc/nginx/ssl:ro + depends_on: + - app + networks: + - ipfspinner-network + restart: unless-stopped + +volumes: + postgres_data: + driver: local + ipfs_data: + driver: local + ipfs_staging: + driver: local + +networks: + ipfspinner-network: + driver: bridge + ipam: + config: + - subnet: 172.20.0.0/16 \ No newline at end of file diff --git a/env.template b/env.template new file mode 100644 index 0000000..e3de0aa --- /dev/null +++ b/env.template @@ -0,0 +1,30 @@ +# IPFSPinner Environment Configuration +# Copy this file to .env and fill in your actual values + +# Database Configuration +DATABASE_URL=postgres://ipfspinner:ipfspinner_password@localhost:5432/ipfspinner?sslmode=disable + +# Stripe Configuration +STRIPE_PUBLIC_KEY=your_stripe_public_key_here +STRIPE_SECRET_KEY=your_stripe_secret_key_here + +# Jackal Blockchain Configuration +JACKAL_SEED=your_jackal_wallet_seed_here + +# IPFS Configuration +IPFS_API_URL=http://localhost:5001 +IPFS_GATEWAY_URL=http://localhost:8080 + +# Application Configuration +APP_PORT=3159 +APP_ENV=development + +# Auth0 Configuration (if using Auth0) +AUTH0_DOMAIN=your_auth0_domain +AUTH0_CLIENT_ID=your_auth0_client_id +AUTH0_CLIENT_SECRET=your_auth0_client_secret +AUTH0_CALLBACK_URL=http://localhost:3159/callback + +# Optional: Logging Configuration +LOG_LEVEL=info +LOG_FORMAT=console \ No newline at end of file diff --git a/init.sql b/init.sql new file mode 100644 index 0000000..72bb833 --- /dev/null +++ b/init.sql @@ -0,0 +1,80 @@ +-- Database initialization script for IPFSPinner +-- This script runs when the PostgreSQL container starts up + +-- Create the files table +CREATE TABLE IF NOT EXISTS files ( + id SERIAL PRIMARY KEY, + user_id TEXT NOT NULL, + file_name TEXT NOT NULL, + size BIGINT, + cid TEXT NOT NULL, + root TEXT NOT NULL, + active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Create the keys table +CREATE TABLE IF NOT EXISTS keys ( + name TEXT NOT NULL, + user_id TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (user_id, name) +); + +-- Create the customers table +CREATE TABLE IF NOT EXISTS customers ( + stripe_id TEXT NOT NULL, + user_id TEXT NOT NULL, + PRIMARY KEY (user_id) +); + +-- Create the subscriptions table +CREATE TABLE IF NOT EXISTS subscriptions ( + sub_id TEXT NOT NULL PRIMARY KEY, + quantity BIGINT, + stripe_id TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Create the usermap table +CREATE TABLE IF NOT EXISTS usermap ( + user_id TEXT NOT NULL PRIMARY KEY, + id TEXT NOT NULL +); + +-- Create the collections table +CREATE TABLE IF NOT EXISTS collections ( + id SERIAL PRIMARY KEY, + collection_name TEXT NOT NULL UNIQUE, + cid TEXT, + user_id TEXT NOT NULL +); + +-- Create the collection_files table +CREATE TABLE IF NOT EXISTS collection_files ( + collection_id INTEGER NOT NULL, + file_id INTEGER NOT NULL, + PRIMARY KEY (collection_id, file_id), + FOREIGN KEY (collection_id) REFERENCES collections (id) ON DELETE CASCADE +); + +-- Create the collection_refs table +CREATE TABLE IF NOT EXISTS collection_refs ( + collection_id INTEGER NOT NULL, + ref_id INTEGER NOT NULL, + PRIMARY KEY (collection_id, ref_id), + FOREIGN KEY (collection_id) REFERENCES collections (id) ON DELETE CASCADE +); + +-- Create indexes for better performance +CREATE INDEX IF NOT EXISTS idx_files_user_id ON files(user_id); +CREATE INDEX IF NOT EXISTS idx_files_cid ON files(cid); +CREATE INDEX IF NOT EXISTS idx_keys_user_id ON keys(user_id); +CREATE INDEX IF NOT EXISTS idx_collections_user_id ON collections(user_id); +CREATE INDEX IF NOT EXISTS idx_collection_files_collection_id ON collection_files(collection_id); +CREATE INDEX IF NOT EXISTS idx_collection_files_file_id ON collection_files(file_id); + +-- Grant permissions to the ipfspinner user +GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO ipfspinner; +GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO ipfspinner; +GRANT ALL PRIVILEGES ON SCHEMA public TO ipfspinner; \ No newline at end of file diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..6b6912b --- /dev/null +++ b/nginx.conf @@ -0,0 +1,149 @@ +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # Logging + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + error_log /var/log/nginx/error.log warn; + + # Basic settings + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + client_max_body_size 100M; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_proxied any; + gzip_comp_level 6; + gzip_types + text/plain + text/css + text/xml + text/javascript + application/json + application/javascript + application/xml+rss + application/atom+xml + image/svg+xml; + + # Rate limiting + limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s; + limit_req_zone $binary_remote_addr zone=upload:10m rate=2r/s; + + # Upstream for the main application + upstream app_backend { + server app:3159; + keepalive 32; + } + + # Upstream for IPFS gateway + upstream ipfs_gateway { + server ipfs:8080; + keepalive 32; + } + + # Main server block + server { + listen 80; + server_name localhost; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + # Health check endpoint + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } + + # IPFS Gateway proxy + location /ipfs/ { + limit_req zone=api burst=20 nodelay; + proxy_pass http://ipfs_gateway; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_connect_timeout 30s; + proxy_send_timeout 30s; + proxy_read_timeout 30s; + } + + # Upload endpoint with rate limiting + location /upload { + limit_req zone=upload burst=5 nodelay; + proxy_pass http://app_backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_connect_timeout 30s; + proxy_send_timeout 300s; + proxy_read_timeout 300s; + } + + # API endpoints + location /api/ { + limit_req zone=api burst=20 nodelay; + proxy_pass http://app_backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_connect_timeout 30s; + proxy_send_timeout 30s; + proxy_read_timeout 30s; + } + + # Web interface and other routes + location / { + proxy_pass http://app_backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_connect_timeout 30s; + proxy_send_timeout 30s; + proxy_read_timeout 30s; + } + + # Error pages + error_page 404 /404.html; + error_page 500 502 503 504 /50x.html; + + location = /50x.html { + root /usr/share/nginx/html; + } + } + + # HTTPS server (uncomment and configure for production) + # server { + # listen 443 ssl http2; + # server_name localhost; + # + # ssl_certificate /etc/nginx/ssl/cert.pem; + # ssl_certificate_key /etc/nginx/ssl/key.pem; + # ssl_protocols TLSv1.2 TLSv1.3; + # ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384; + # ssl_prefer_server_ciphers off; + # + # # Include the same location blocks as above + # } +} \ No newline at end of file From 8c1dd820483e7183d210ce12672bcd3ae8ad710d Mon Sep 17 00:00:00 2001 From: marston Date: Thu, 21 Aug 2025 17:17:10 -0400 Subject: [PATCH 2/4] tweaking --- .gitmodules | 3 +++ colada | 1 + 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 colada diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..666c732 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "colada"] + path = colada + url = https://github.com/JackalLabs/colada.git diff --git a/colada b/colada new file mode 160000 index 0000000..6798daf --- /dev/null +++ b/colada @@ -0,0 +1 @@ +Subproject commit 6798daf7e19b3580a37c0f566b0e44ed9a4f2388 From a9c9cb640785f8cfdebab95f0f474f7775e22994 Mon Sep 17 00:00:00 2001 From: marston Date: Thu, 21 Aug 2025 17:17:11 -0400 Subject: [PATCH 3/4] tweakin --- DOCKER_README.md | 18 +- Dockerfile | 2 +- Makefile | 56 +++-- colada | 2 +- docker-compose.yml | 49 ++-- env.template | 9 + go.mod | 52 ++--- go.sum | 77 +++--- jackal/uploader/kubo_upload.go | 222 ++++++++++++++++++ jackal/uploader/upload.go | 4 + main.go | 24 +- platform/collections/collections.go | 4 + platform/middleware/isAuthenticated.go | 5 +- platform/payment/accounts.go | 4 +- platform/payment/stripe.go | 5 +- platform/router/router.go | 60 +++-- utils/files.go | 2 + utils/kubo.go | 312 +++++++++++++++++++++++++ utils/usage.go | 1 + web/app/items/items.go | 1 + web/app/items/jitems.go | 4 + web/app/keys/keys.go | 4 +- web/app/upload/upload.go | 146 ++++++++---- 23 files changed, 880 insertions(+), 183 deletions(-) create mode 100644 jackal/uploader/kubo_upload.go create mode 100644 utils/kubo.go diff --git a/DOCKER_README.md b/DOCKER_README.md index 0d5b311..1e2d6d9 100644 --- a/DOCKER_README.md +++ b/DOCKER_README.md @@ -56,12 +56,18 @@ The Docker Compose setup includes the following services: - **Data Volume:** `ipfs_data` - **Staging Volume:** `ipfs_staging` -### 3. IPFSPinner Application (`app`) +### 3. Colada Frontend (`colada`) +- **Port:** 3049 +- **Type:** Vue.js SPA with Vite (Development Mode) +- **Features:** Hot reload, SPA routing, API proxy to backend +- **Environment:** Uses `VITE_API_BASE_URL` for backend API calls + +### 4. IPFSPinner Application (`app`) - **Port:** 3159 - **Depends on:** postgres, ipfs - **Uploads Volume:** `./uploads` -### 4. Nginx Reverse Proxy (`nginx`) - Optional +### 5. Nginx Reverse Proxy (`nginx`) - Optional - **Ports:** 80, 443 - **Depends on:** app @@ -72,6 +78,8 @@ Create a `.env` file based on `env.template` with the following required variabl - `DATABASE_URL`: PostgreSQL connection string - `STRIPE_PUBLIC_KEY`: Your Stripe public key - `JACKAL_SEED`: Your Jackal wallet seed phrase +- `JACKAL_RPC_URL`: Jackal blockchain RPC endpoint (e.g., http://35.193.64.216:26657) +- `JACKAL_GRPC_URL`: Jackal blockchain gRPC endpoint (e.g., 35.193.64.216:9090) ## Useful Commands @@ -119,6 +127,7 @@ docker-compose exec postgres psql -U ipfspinner -d ipfspinner # Database shell - Application: http://localhost:3159 - IPFS Gateway: http://localhost:8080 - IPFS API: http://localhost:5001 + - Colada Frontend: http://localhost:3049 - Database: localhost:5432 ## Database Management @@ -167,6 +176,11 @@ This configures the IPFS node to allow cross-origin requests from your applicati 2. Verify IPFS node is accessible: `curl http://localhost:5001/api/v0/version` 3. Check if IPFS data volume has proper permissions +### Colada Frontend issues +1. Check Colada logs: `make logs-colada` +2. Verify frontend is accessible: `curl http://localhost:3000/health` +3. Rebuild frontend if needed: `make build-colada` + ### Application issues 1. Check application logs: `make logs-app` 2. Verify all environment variables are set diff --git a/Dockerfile b/Dockerfile index 5ad8fc3..4b6b1fb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build stage -FROM golang:1.22-alpine AS builder +FROM golang:1.23-alpine AS builder # Install build dependencies RUN apk add --no-cache git ca-certificates tzdata diff --git a/Makefile b/Makefile index 8d02456..5bd52ce 100644 --- a/Makefile +++ b/Makefile @@ -87,81 +87,87 @@ help: # Build the Docker images build: - docker-compose build + docker compose build # Start all services up: - docker-compose up -d + docker compose up -d # Start all services and show logs up-logs: - docker-compose up + docker compose up # Stop all services down: - docker-compose down + docker compose down # Show logs from all services logs: - docker-compose logs -f + docker compose logs -f # Show logs from specific service logs-app: - docker-compose logs -f app + docker compose logs -f app logs-db: - docker-compose logs -f postgres + docker compose logs -f postgres logs-ipfs: - docker-compose logs -f ipfs + docker compose logs -f ipfs -# Remove all containers, networks, and volumes -clean: - docker-compose down -v --remove-orphans - docker system prune -f +logs-colada: + docker compose logs -f colada # Restart all services restart: - docker-compose restart + docker compose restart # Open shell in the application container shell-app: - docker-compose exec app sh + docker compose exec app sh # Open shell in the PostgreSQL container shell-db: - docker-compose exec postgres psql -U ipfspinner -d ipfspinner + docker compose exec postgres psql -U ipfspinner -d ipfspinner # Open shell in the IPFS container shell-ipfs: - docker-compose exec ipfs sh + docker compose exec ipfs sh + +# Open shell in the Colada container +shell-colada: + docker compose exec colada sh + +# Build Colada frontend +build-colada: + docker compose build colada # Run tests in the application container test: - docker-compose exec app go test ./... + docker compose exec app go test ./... # Check service status status: - docker-compose ps + docker compose ps # View service health health: - docker-compose ps --format "table {{.Name}}\t{{.Status}}\t{{.Ports}}" + docker compose ps --format "table {{.Name}}\t{{.Status}}\t{{.Ports}}" # Scale services (e.g., make scale-app=3) scale-app: - docker-compose up -d --scale app=$(scale-app) + docker compose up -d --scale app=$(scale-app) # Backup database backup-db: - docker-compose exec postgres pg_dump -U ipfspinner ipfspinner > backup_$(shell date +%Y%m%d_%H%M%S).sql + docker compose exec postgres pg_dump -U ipfspinner ipfspinner > backup_$(shell date +%Y%m%d_%H%M%S).sql # Restore database from backup restore-db: - docker-compose exec -T postgres psql -U ipfspinner -d ipfspinner < $(backup_file) + docker compose exec -T postgres psql -U ipfspinner -d ipfspinner < $(backup_file) # Initialize IPFS node (first time setup) init-ipfs: - docker-compose exec ipfs ipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin '["*"]' - docker-compose exec ipfs ipfs config --json API.HTTPHeaders.Access-Control-Allow-Methods '["PUT", "POST", "GET"]' - docker-compose exec ipfs ipfs config --json API.HTTPHeaders.Access-Control-Allow-Headers '["Authorization"]' + docker compose exec ipfs ipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin '["*"]' + docker compose exec ipfs ipfs config --json API.HTTPHeaders.Access-Control-Allow-Methods '["PUT", "POST", "GET"]' + docker compose exec ipfs ipfs config --json API.HTTPHeaders.Access-Control-Allow-Headers '["Authorization"]' diff --git a/colada b/colada index 6798daf..74c0243 160000 --- a/colada +++ b/colada @@ -1 +1 @@ -Subproject commit 6798daf7e19b3580a37c0f566b0e44ed9a4f2388 +Subproject commit 74c02430ee21ba7f5664c60757aa444e382a7822 diff --git a/docker-compose.yml b/docker-compose.yml index 55fd400..fa9e3a8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,4 @@ -version: '3.8' + services: # PostgreSQL Database @@ -46,6 +46,32 @@ services: retries: 3 restart: unless-stopped + # Colada Frontend (Vue.js) + colada: + build: + context: ./colada + dockerfile: Dockerfile + container_name: ipfspinner-colada + ports: + - "3049:3049" # Frontend port + environment: + - VITE_AUTH0_CLIENT_ID=${AUTH0_CLIENT_ID} + - VITE_AUTH0_DOMAIN=${AUTH0_DOMAIN} + - VITE_AUTH0_CLIENT_SECRET=${AUTH0_CLIENT_SECRET} + - VITE_AUTH0_CALLBACK_URL=${AUTH0_CALLBACK_URL} + - VITE_AUTH0_AUDIENCE=${AUTH0_AUDIENCE} + - VITE_STRIPE_KEY=${VITE_STRIPE_KEY} + - VITE_STRIPE_CODE=${VITE_STRIPE_CODE} + - VITE_API_BASE_URL=${API_DOMAIN} + networks: + - ipfspinner-network + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost/health"] + interval: 30s + timeout: 10s + retries: 3 + restart: unless-stopped + # IPFSPinner Application app: build: @@ -59,6 +85,7 @@ services: - JACKAL_SEED=${JACKAL_SEED} - IPFS_API_URL=http://ipfs:5001 - IPFS_GATEWAY_URL=http://ipfs:8080 + - AUTH0_CLIENT_ID=${AUTH0_CLIENT_ID} - AUTH0_DOMAIN=${AUTH0_DOMAIN} - AUTH0_CLIENT_SECRET=${AUTH0_CLIENT_SECRET} @@ -67,7 +94,8 @@ services: - SERVER_DOMAIN=${SERVER_DOMAIN} - STRIPE_SOCKET_SECRET=${STRIPE_SOCKET_SECRET} - STRIPE_PUBLIC_KEY=${STRIPE_PUBLIC_KEY} - + - JACKAL_RPC_URL=${JACKAL_RPC_URL} + - JACKAL_GRPC_URL=${JACKAL_GRPC_URL} volumes: - ./uploads:/app/uploads depends_on: @@ -84,22 +112,6 @@ services: timeout: 10s retries: 3 - # Nginx Reverse Proxy (Optional) - nginx: - image: nginx:alpine - container_name: ipfspinner-nginx - ports: - - "80:80" - - "443:443" - volumes: - - ./nginx.conf:/etc/nginx/nginx.conf:ro - - ./ssl:/etc/nginx/ssl:ro - depends_on: - - app - networks: - - ipfspinner-network - restart: unless-stopped - volumes: postgres_data: driver: local @@ -108,6 +120,7 @@ volumes: ipfs_staging: driver: local + networks: ipfspinner-network: driver: bridge diff --git a/env.template b/env.template index e3de0aa..1337cc0 100644 --- a/env.template +++ b/env.template @@ -10,11 +10,20 @@ STRIPE_SECRET_KEY=your_stripe_secret_key_here # Jackal Blockchain Configuration JACKAL_SEED=your_jackal_wallet_seed_here +JACKAL_RPC_URL=http://35.193.64.216:26657 +JACKAL_GRPC_URL=35.193.64.216:9090 # IPFS Configuration IPFS_API_URL=http://localhost:5001 IPFS_GATEWAY_URL=http://localhost:8080 +# Colada Frontend +# Frontend runs on http://localhost:3049 +VITE_STRIPE_KEY=your_stripe_public_key_here +VITE_STRIPE_CODE=your_stripe_code_here +VITE_API_BASE_URL=http://localhost:3159 +VITE_IPFS_GATEWAY_URL=http://localhost:8080/ipfs/ + # Application Configuration APP_PORT=3159 APP_ENV=development diff --git a/go.mod b/go.mod index 2d5e8fe..273b307 100644 --- a/go.mod +++ b/go.mod @@ -1,24 +1,28 @@ module jackalnft -go 1.22.0 +go 1.23.0 -toolchain go1.23.4 +toolchain go1.24.1 require ( github.com/auth0/go-jwt-middleware/v2 v2.2.2 github.com/coreos/go-oidc/v3 v3.9.0 github.com/cosmos/cosmos-sdk v0.45.17 github.com/desmos-labs/cosmos-go-wallet v0.0.0-00010101000000-000000000000 - github.com/gin-contrib/cors v1.5.0 + github.com/gin-contrib/cors v1.7.6 github.com/gin-contrib/sessions v0.0.4 - github.com/gin-gonic/gin v1.10.0 + github.com/gin-gonic/gin v1.10.1 github.com/golang-jwt/jwt/v5 v5.2.1 github.com/hsanjuan/ipfs-lite v1.8.2 + github.com/ipfs/boxo v0.17.0 + github.com/ipfs/go-cid v0.4.1 + github.com/ipfs/go-ipld-format v0.6.0 github.com/jackalLabs/canine-chain/v3 v3.0.0-00010101000000-000000000000 github.com/joho/godotenv v1.5.1 github.com/lib/pq v1.10.7 github.com/libp2p/go-libp2p v0.32.2 github.com/multiformats/go-multiaddr v0.12.1 + github.com/multiformats/go-multihash v0.2.3 github.com/rs/zerolog v1.29.1 github.com/stripe/stripe-go/v81 v81.2.0 github.com/swaggest/swgui v1.8.2 @@ -89,9 +93,9 @@ require ( github.com/flynn/noise v1.0.1 // indirect github.com/francoispqt/gojay v1.2.13 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/gabriel-vasile/mimetype v1.4.9 // indirect github.com/getsentry/sentry-go v0.18.0 // indirect - github.com/gin-contrib/sse v1.0.0 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect github.com/go-jose/go-jose/v3 v3.0.1 // indirect github.com/go-kit/kit v0.12.0 // indirect github.com/go-kit/log v0.2.1 // indirect @@ -100,18 +104,18 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.24.0 // indirect + github.com/go-playground/validator/v10 v10.26.0 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect - github.com/goccy/go-json v0.10.4 // indirect + github.com/goccy/go-json v0.10.5 // indirect github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/gogo/gateway v1.1.0 // indirect github.com/gogo/protobuf v1.3.3 // indirect github.com/golang/glog v1.1.2 // indirect - github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/btree v1.1.2 // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/gopacket v1.1.19 // indirect github.com/google/orderedcode v0.0.1 // indirect @@ -139,16 +143,13 @@ require ( github.com/improbable-eng/grpc-web v0.15.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/ipfs/bbloom v0.0.4 // indirect - github.com/ipfs/boxo v0.17.0 // indirect github.com/ipfs/go-bitfield v1.1.0 // indirect github.com/ipfs/go-block-format v0.2.0 // indirect - github.com/ipfs/go-cid v0.4.1 // indirect github.com/ipfs/go-cidutil v0.1.0 // indirect github.com/ipfs/go-datastore v0.6.0 // indirect github.com/ipfs/go-ipfs-delay v0.0.1 // indirect github.com/ipfs/go-ipfs-pq v0.0.3 // indirect github.com/ipfs/go-ipfs-util v0.0.3 // indirect - github.com/ipfs/go-ipld-format v0.6.0 // indirect github.com/ipfs/go-ipld-legacy v0.2.1 // indirect github.com/ipfs/go-log v1.0.5 // indirect github.com/ipfs/go-log/v2 v2.5.1 // indirect @@ -162,7 +163,7 @@ require ( github.com/jmhodges/levigo v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.17.4 // indirect - github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/koron/go-ssdp v0.0.4 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect @@ -203,7 +204,6 @@ require ( github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect github.com/multiformats/go-multibase v0.2.0 // indirect github.com/multiformats/go-multicodec v0.9.0 // indirect - github.com/multiformats/go-multihash v0.2.3 // indirect github.com/multiformats/go-multistream v0.5.0 // indirect github.com/multiformats/go-varint v0.0.7 // indirect github.com/onsi/ginkgo/v2 v2.13.2 // indirect @@ -212,7 +212,7 @@ require ( github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect github.com/pelletier/go-toml v1.9.5 // indirect - github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/petermattis/goid v0.0.0-20230317030725-371a4b8eda08 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect @@ -247,7 +247,7 @@ require ( github.com/tendermint/tendermint v0.34.27 // indirect github.com/tendermint/tm-db v0.6.7 // indirect github.com/tidwall/btree v1.5.0 // indirect - github.com/ugorji/go/codec v1.2.12 // indirect + github.com/ugorji/go/codec v1.3.0 // indirect github.com/vearutop/statigz v1.4.0 // indirect github.com/wealdtech/go-merkletree/v2 v2.5.1-0.20231106114422-6769f4468d71 // indirect github.com/whyrusleeping/chunker v0.0.0-20181014151217-fe64bd25879f // indirect @@ -264,22 +264,22 @@ require ( go.uber.org/mock v0.4.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.26.0 // indirect - golang.org/x/crypto v0.32.0 // indirect + golang.org/x/crypto v0.39.0 // indirect golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc // indirect - golang.org/x/mod v0.22.0 // indirect - golang.org/x/net v0.34.0 // indirect - golang.org/x/sync v0.10.0 // indirect - golang.org/x/sys v0.29.0 // indirect - golang.org/x/term v0.28.0 // indirect - golang.org/x/text v0.21.0 // indirect - golang.org/x/tools v0.29.0 // indirect + golang.org/x/mod v0.25.0 // indirect + golang.org/x/net v0.41.0 // indirect + golang.org/x/sync v0.15.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/term v0.32.0 // indirect + golang.org/x/text v0.26.0 // indirect + golang.org/x/tools v0.33.0 // indirect gonum.org/v1/gonum v0.14.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto v0.0.0-20240102182953-50ed04b92917 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240108191215-35c7eff3a6b1 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1 // indirect google.golang.org/grpc v1.61.1 // indirect - google.golang.org/protobuf v1.32.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect gopkg.in/go-jose/go-jose.v2 v2.6.3 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 91b27ed..094fe53 100644 --- a/go.sum +++ b/go.sum @@ -466,8 +466,8 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4 github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= -github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= -github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= +github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= github.com/garslo/gogen v0.0.0-20170306192744-1d203ffc1f61/go.mod h1:Q0X6pkwTILDlzrGEckF6HKjXe48EgsY/l7K7vhY4MW8= github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= @@ -479,13 +479,13 @@ github.com/getsentry/sentry-go v0.18.0 h1:MtBW5H9QgdcJabtZcuJG80BMOwaBpkRDZkxRkN github.com/getsentry/sentry-go v0.18.0/go.mod h1:Kgon4Mby+FJ7ZWHFUAZgVaIa8sxHtnRJRLTXZr51aKQ= github.com/ghemawat/stream v0.0.0-20171120220530-696b145b53b9/go.mod h1:106OIgooyS7OzLDOpUGgm9fA3bQENb/cFSyyBmMoJDs= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/gin-contrib/cors v1.5.0 h1:DgGKV7DDoOn36DFkNtbHrjoRiT5ExCe+PC9/xp7aKvk= -github.com/gin-contrib/cors v1.5.0/go.mod h1:TvU7MAZ3EwrPLI2ztzTt3tqgvBCq+wn8WpZmfADjupI= +github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY= +github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk= github.com/gin-contrib/sessions v0.0.4 h1:gq4fNa1Zmp564iHP5G6EBuktilEos8VKhe2sza1KMgo= github.com/gin-contrib/sessions v0.0.4/go.mod h1:pQ3sIyviBBGcxgyR8mkeJuXbeV3h3NYmhJADQTq5+Vo= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E= -github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8= github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= @@ -537,8 +537,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= -github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg= -github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus= +github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= +github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= @@ -561,8 +561,8 @@ github.com/gobwas/ws v1.2.1 h1:F2aeBZrm2NDsc7vbovKrWSogd4wvfAxg0FQ89/iqOTk= github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= -github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -624,8 +624,8 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= @@ -652,8 +652,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= @@ -955,8 +955,8 @@ github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= github.com/klauspost/cpuid v0.0.0-20170728055534-ae7887de9fa5/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= -github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= -github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/crc32 v0.0.0-20161016154125-cb6bfca970f6/go.mod h1:+ZoRqAPRLkC4NPOvfYeR5KNOrY6TD+/sAC3HXPZgDYg= github.com/klauspost/pgzip v1.0.2-0.20170402124221-0bf5dcad4ada/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -1249,8 +1249,8 @@ github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3v github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= -github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= -github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= github.com/peterh/liner v1.0.1-0.20180619022028-8c1271fcf47f/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc= github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= @@ -1489,7 +1489,6 @@ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1F github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stripe/stripe-go/v81 v81.2.0 h1:AduJoFed6xif3uG7rXRa2LxY+AJiialVA1hXDak1aUk= @@ -1541,8 +1540,8 @@ github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6 github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= -github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= -github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= +github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= @@ -1702,8 +1701,8 @@ golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= -golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= -golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1755,8 +1754,8 @@ golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57/go.mod h1:3p9vT2HGsQu2 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= -golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1833,8 +1832,8 @@ golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= -golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= -golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -1869,8 +1868,8 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1990,8 +1989,8 @@ golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -1999,8 +1998,8 @@ golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= -golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= -golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -2015,8 +2014,8 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -2097,8 +2096,8 @@ golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023/go.mod h1:nABZi5QlRsZVlz golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE= -golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588= +golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= +golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -2260,8 +2259,8 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= -google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/jackal/uploader/kubo_upload.go b/jackal/uploader/kubo_upload.go new file mode 100644 index 0000000..eaf2d9b --- /dev/null +++ b/jackal/uploader/kubo_upload.go @@ -0,0 +1,222 @@ +package uploader + +import ( + "bytes" + "context" + "database/sql" + "encoding/hex" + "fmt" + "io" + + "jackalnft/utils" + + "github.com/gin-gonic/gin" + "github.com/rs/zerolog/log" + + "github.com/desmos-labs/cosmos-go-wallet/wallet" +) + +// KuboUploader handles file uploads using Kubo instead of IPFS lite +type KuboUploader struct { + kuboClient *utils.KuboClient +} + +// NewKuboUploader creates a new Kubo uploader +func NewKuboUploader(kuboClient *utils.KuboClient) *KuboUploader { + return &KuboUploader{ + kuboClient: kuboClient, + } +} + +// ProcessFileWithKubo processes a file using Kubo and saves it to the database +func (ku *KuboUploader) ProcessFileWithKubo(ctx *gin.Context, fileName string, data []byte, folder bool, cidToMatch string) (string, []byte, int, error) { + // Use Kubo client to add file and generate CID + var cidString string + var err error + + // Add file to Kubo + response, err := ku.kuboClient.AddFile(ctx, bytes.NewBuffer(data), fileName) + if err != nil { + return "", nil, 0, fmt.Errorf("failed to add file to Kubo: %w", err) + } + + cidString = response.Hash + + // If CID matching is required, try with raw leaves if needed + if len(cidToMatch) > 0 && cidString != cidToMatch { + // For now, just return an error if CID doesn't match + // In the future, we could implement raw leaves logic with Kubo + return "", nil, 0, fmt.Errorf("CID mismatch: got %s, expected %s", cidString, cidToMatch) + } + + log.Info().Msgf("CID: %s", cidString) + // Pin the file in Kubo + go func() { + log.Info().Msgf("Pinning file in Kubo: %s", cidString) + if err := ku.kuboClient.PinCID(context.Background(), cidString); err != nil { + log.Error().Err(err).Msg("Failed to pin file in Kubo") + } else { + log.Info().Msgf("Successfully pinned file in Kubo: %s", cidString) + } + }() + + w := ctx.MustGet("wallet").(*wallet.Wallet) + + buf := bytes.NewBuffer(data) + fileData := bytes.NewBuffer(buf.Bytes()) + + size, root, err := BuildTree(ctx, buf, w) + if err != nil { + fmt.Println("can't build tree err: ", err.Error()) + return "", nil, 0, err + } + + // Post file to blockchain in background + go func() { + log.Printf("Posting file now: %s", cidString) + _, _, err := PostFile(ctx, fileName, fileData.Bytes(), root, size, w, folder, cidToMatch) + if err != nil { + log.Print(err) + return + } + }() + + return cidString, root, size, nil +} + +// CloneFromIPFS clones a file from IPFS using Kubo and saves it to the database +func (ku *KuboUploader) CloneFromIPFS(ctx *gin.Context, cidStr string, userID string) error { + // First, pin the CID in Kubo + if err := ku.kuboClient.PinCID(ctx, cidStr); err != nil { + return fmt.Errorf("failed to pin CID in Kubo: %w", err) + } + + // Check if it's a directory + isDir, err := ku.kuboClient.IsDirectory(ctx, cidStr) + if err != nil { + log.Printf("Error checking if CID is directory: %v", err) + // Continue as file, we'll handle errors later + } + + if isDir { + return ku.saveDirectoryFromIPFS(ctx, cidStr, userID, 0) + } + + return ku.saveFileFromIPFS(ctx, cidStr, userID) +} + +// saveDirectoryFromIPFS saves a directory from IPFS to the database +func (ku *KuboUploader) saveDirectoryFromIPFS(ctx *gin.Context, cidStr string, userID string, parentID int64) error { + db := ctx.MustGet("db").(*sql.DB) + + // Create collection for the directory + var id int64 + err := db.QueryRow("INSERT INTO collections (collection_name, user_id) VALUES ($1, $2) RETURNING id", cidStr, userID).Scan(&id) + if err != nil { + return fmt.Errorf("failed to create collection: %w", err) + } + + // If this is a subdirectory, link it to parent + if parentID > 0 { + query := ` + INSERT INTO collection_refs (collection_id, ref_id) + SELECT $1, $2 + FROM collections + WHERE id = $1 AND user_id = $3 + ` + _, err = db.Exec(query, parentID, id, userID) + if err != nil { + return fmt.Errorf("failed to link collection to parent: %w", err) + } + } + + // Get directory contents + contents, err := ku.kuboClient.GetDirectoryContents(ctx, cidStr) + if err != nil { + return fmt.Errorf("failed to get directory contents: %w", err) + } + + // Process each item in the directory + for _, item := range contents { + // Check if item is a directory + isItemDir, err := ku.kuboClient.IsDirectory(ctx, item.Hash) + if err != nil { + log.Printf("Error checking if item is directory: %v", err) + continue + } + + if isItemDir { + // Recursively process subdirectory + err = ku.saveDirectoryFromIPFS(ctx, item.Hash, userID, id) + if err != nil { + log.Printf("Error processing subdirectory %s: %v", item.Name, err) + continue + } + } else { + // Process file + err = ku.saveFileFromIPFS(ctx, item.Hash, userID) + if err != nil { + log.Printf("Error processing file %s: %v", item.Name, err) + continue + } + } + } + + // Update collection metadata + _, err = db.Exec("UPDATE collections SET updated_at = NOW() WHERE id = $1", id) + if err != nil { + log.Printf("Error updating collection: %v", err) + } + + return nil +} + +// saveFileFromIPFS saves a file from IPFS to the database +func (ku *KuboUploader) saveFileFromIPFS(ctx *gin.Context, cidStr string, userID string) error { + db := ctx.MustGet("db").(*sql.DB) + + // Get file info + fileInfo, err := ku.kuboClient.GetFileInfo(ctx, cidStr) + if err != nil { + return fmt.Errorf("failed to get file info: %w", err) + } + + // Get file data + fileData, err := ku.kuboClient.GetFile(ctx, cidStr) + if err != nil { + return fmt.Errorf("failed to get file data: %w", err) + } + // nolint:errcheck + defer fileData.Close() + + // Read file data + data, err := io.ReadAll(fileData) + if err != nil { + return fmt.Errorf("failed to read file data: %w", err) + } + + // Generate CID and process file + fileName := fileInfo.Name + if fileName == "" { + fileName = cidStr + } + + // Use the existing ProcessFile function to handle the blockchain posting + cid, root, size, err := ProcessFile(ctx, fileName, data, false, cidStr) + if err != nil { + return fmt.Errorf("failed to process file: %w", err) + } + + // Convert root to hex string + merkle := hex.EncodeToString(root) + + // Insert file metadata into the database + _, err = db.Exec("INSERT INTO files (user_id, file_name, cid, root, size) VALUES ($1, $2, $3, $4, $5)", + userID, fileName, cid, merkle, size) + if err != nil { + return fmt.Errorf("failed to save file record: %w", err) + } + + log.Printf("Successfully saved file from IPFS: %s (CID: %s, Size: %d)", fileName, cid, size) + return nil +} diff --git a/jackal/uploader/upload.go b/jackal/uploader/upload.go index 2d49280..584867d 100644 --- a/jackal/uploader/upload.go +++ b/jackal/uploader/upload.go @@ -53,6 +53,8 @@ func uploadFile(ip string, r io.Reader, merkle []byte, start int64, address stri var b bytes.Buffer writer := multipart.NewWriter(&b) + + // nolint:errcheck defer writer.Close() err = writer.WriteField("sender", address) @@ -84,6 +86,7 @@ func uploadFile(ip string, r io.Reader, merkle []byte, start int64, address stri if err != nil { return "", err } + // nolint:errcheck writer.Close() req, _ := http.NewRequest("POST", u.String(), &b) @@ -94,6 +97,7 @@ func uploadFile(ip string, r io.Reader, merkle []byte, start int64, address stri return "", err } + // nolint:errcheck defer res.Body.Close() if res.StatusCode != 200 { diff --git a/main.go b/main.go index b24c2fd..7868a7e 100644 --- a/main.go +++ b/main.go @@ -17,22 +17,24 @@ import ( "jackalnft/utils" - "github.com/desmos-labs/cosmos-go-wallet/types" - "github.com/desmos-labs/cosmos-go-wallet/wallet" "jackalnft/jackal/uploader" jWallet "jackalnft/jackal/wallet" + "github.com/desmos-labs/cosmos-go-wallet/types" + "github.com/desmos-labs/cosmos-go-wallet/wallet" + "github.com/joho/godotenv" _ "github.com/lib/pq" - storageTypes "github.com/jackalLabs/canine-chain/v3/x/storage/types" "jackalnft/platform/authenticator" "jackalnft/platform/router" + + storageTypes "github.com/jackalLabs/canine-chain/v3/x/storage/types" ) var ( - RPC = "http://35.193.64.216:26657" - GRPC = "35.193.64.216:9090" + RPC = utils.GetEnv("JACKAL_RPC_URL") + GRPC = utils.GetEnv("JACKAL_GRPC_URL") ) func init() { @@ -113,6 +115,7 @@ func main() { if err != nil { panic(err) } + // nolint:errcheck defer db.Close() quit := make(chan os.Signal, 1) @@ -133,7 +136,8 @@ func main() { err = db.QueryRow("SELECT SUM(size) FROM files").Scan(&totalSize) if err != nil { log.Warn().Msg(err.Error()) - return + time.Sleep(time.Minute * 30) // wait half hour before checking again + continue } log.Info().Msgf("Currently using %d bytes.", totalSize) @@ -147,9 +151,13 @@ func main() { q := uploader.NewQueue(w) q.Listen() - peer := utils.MakeIPFS() + // Initialize Kubo client instead of IPFS lite peer + kuboClient := utils.NewKuboClient( + utils.GetEnv("IPFS_API_URL"), + utils.GetEnv("IPFS_GATEWAY_URL"), + ) - rtr := router.New(auth, db, w, q, peer) + rtr := router.New(auth, db, w, q, kuboClient) srv := &http.Server{ Addr: "0.0.0.0:3159", diff --git a/platform/collections/collections.go b/platform/collections/collections.go index 25458c0..a294f9a 100644 --- a/platform/collections/collections.go +++ b/platform/collections/collections.go @@ -148,6 +148,7 @@ func GetCollectionParents(db *sql.DB, collectionId int64) ([]int64, error) { if err != nil { return collections, err } + // nolint:errcheck defer rows.Close() for rows.Next() { @@ -173,6 +174,7 @@ func GetCollectionChildren(db *sql.DB, collectionId int64) ([]types.Collection, if err != nil { return collections, err } + // nolint:errcheck defer rows.Close() for rows.Next() { @@ -368,6 +370,7 @@ func ListHandler(ctx *gin.Context) { return } rows = r + // nolint:errcheck defer r.Close() } else { r, err := db.Query("SELECT id, collection_name, cid FROM collections WHERE user_id = $1 ORDER BY id DESC LIMIT $2 OFFSET $3", sub, limit, offset) @@ -376,6 +379,7 @@ func ListHandler(ctx *gin.Context) { return } rows = r + // nolint:errcheck defer r.Close() } diff --git a/platform/middleware/isAuthenticated.go b/platform/middleware/isAuthenticated.go index a2fbc94..26c9118 100644 --- a/platform/middleware/isAuthenticated.go +++ b/platform/middleware/isAuthenticated.go @@ -14,9 +14,10 @@ import ( "github.com/auth0/go-jwt-middleware/v2/jwks" "github.com/auth0/go-jwt-middleware/v2/validator" + "jackalnft/types" + "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" - "jackalnft/types" ) var jwtSecret = []byte("jackal_ipfs_secret_key") @@ -40,6 +41,7 @@ func getEmail(accessToken string) (string, error) { if err != nil { return "", err } + // nolint:errcheck defer resp.Body.Close() // Read response body @@ -160,6 +162,7 @@ func IsAuthenticated(ctx *gin.Context) { ctx.JSON(http.StatusInternalServerError, gin.H{"message": fmt.Sprintf("query error: %s", err.Error())}) return } + // nolint:errcheck defer rows.Close() if !rows.Next() { diff --git a/platform/payment/accounts.go b/platform/payment/accounts.go index faae560..58cd38c 100644 --- a/platform/payment/accounts.go +++ b/platform/payment/accounts.go @@ -9,10 +9,11 @@ import ( "jackalnft/utils" + "jackalnft/types" + "github.com/gin-gonic/gin" "github.com/stripe/stripe-go/v81" "github.com/stripe/stripe-go/v81/customer" - "jackalnft/types" ) func NewAccountHandler(ctx *gin.Context) { @@ -29,6 +30,7 @@ func NewAccountHandler(ctx *gin.Context) { ctx.JSON(http.StatusNotFound, gin.H{"message": "error loading db"}) return } + // nolint:errcheck defer rows.Close() var stripeID string if rows.Next() { diff --git a/platform/payment/stripe.go b/platform/payment/stripe.go index 8a6e42a..93c827c 100644 --- a/platform/payment/stripe.go +++ b/platform/payment/stripe.go @@ -8,12 +8,13 @@ import ( "jackalnft/utils" + "jackalnft/types" + "github.com/gin-gonic/gin" "github.com/stripe/stripe-go/v81" billSession "github.com/stripe/stripe-go/v81/billingportal/session" "github.com/stripe/stripe-go/v81/checkout/session" "github.com/stripe/stripe-go/v81/price" - "jackalnft/types" ) func ManageBillingHandler(ctx *gin.Context) { @@ -32,6 +33,7 @@ func ManageBillingHandler(ctx *gin.Context) { ctx.JSON(http.StatusInternalServerError, gin.H{"message": fmt.Sprintf("query error: %s", err.Error())}) return } + // nolint:errcheck defer rows.Close() var stripeID string @@ -86,6 +88,7 @@ func CheckoutSessionHandler(ctx *gin.Context) { ctx.JSON(http.StatusInternalServerError, gin.H{"message": fmt.Sprintf("query error: %s", err.Error())}) return } + // nolint:errcheck defer rows.Close() var stripeID string diff --git a/platform/router/router.go b/platform/router/router.go index 221f708..178bcf1 100644 --- a/platform/router/router.go +++ b/platform/router/router.go @@ -8,25 +8,27 @@ import ( "net/http" "jackalnft/platform/collections" + "jackalnft/utils" - "github.com/desmos-labs/cosmos-go-wallet/wallet" - "github.com/gin-contrib/sessions" - "github.com/gin-contrib/sessions/cookie" - "github.com/gin-gonic/gin" - ipfslite "github.com/hsanjuan/ipfs-lite" - "github.com/swaggest/swgui/v5emb" "jackalnft/jackal/uploader" "jackalnft/platform/payment" "jackalnft/web/app/items" "jackalnft/web/app/keys" "jackalnft/web/app/upload" + "github.com/desmos-labs/cosmos-go-wallet/wallet" + "github.com/gin-contrib/cors" + "github.com/gin-contrib/sessions" + "github.com/gin-contrib/sessions/cookie" + "github.com/gin-gonic/gin" + "github.com/swaggest/swgui/v5emb" + "jackalnft/platform/authenticator" "jackalnft/platform/middleware" ) // New registers the routes and returns the router. -func New(auth *authenticator.Authenticator, db *sql.DB, wallet *wallet.Wallet, queue *uploader.Queue, peer *ipfslite.Peer) *gin.Engine { +func New(auth *authenticator.Authenticator, db *sql.DB, wallet *wallet.Wallet, queue *uploader.Queue, kuboClient *utils.KuboClient) *gin.Engine { router := gin.Default() router.Use(func(c *gin.Context) { @@ -44,10 +46,10 @@ func New(auth *authenticator.Authenticator, db *sql.DB, wallet *wallet.Wallet, q c.Next() }) - //router.Use(func(c *gin.Context) { - // c.Set("ipfs", peer) - // c.Next() - //}) + router.Use(func(c *gin.Context) { + c.Set("kubo", kuboClient) + c.Next() + }) // To store custom types in our cookies, // we must first register them using gob.Register @@ -55,11 +57,13 @@ func New(auth *authenticator.Authenticator, db *sql.DB, wallet *wallet.Wallet, q store := cookie.NewStore([]byte("secret")) router.Use(sessions.Sessions("auth-session", store)) - //config := cors.DefaultConfig() - //config.AllowAllOrigins = true - //config.AllowHeaders = append(config.AllowHeaders, "Authorization") - // - //router.Use(cors.New(config)) + config := cors.DefaultConfig() + config.AllowAllOrigins = true + config.AllowHeaders = append(config.AllowHeaders, "Authorization", "Content-Type") + config.AllowMethods = append(config.AllowMethods, "GET", "POST", "PUT", "DELETE", "OPTIONS") + config.AllowCredentials = true + + router.Use(cors.New(config)) router.Static("/public", "web/static") // router.LoadHTMLGlob("web/template/*") @@ -83,10 +87,10 @@ func New(auth *authenticator.Authenticator, db *sql.DB, wallet *wallet.Wallet, q router.POST("/api/clone", middleware.IsAuthenticated, upload.CloneHandler) // clone file from URL router.POST("/api/v1/clone", middleware.IsAuthenticated, upload.V1CloneHandler) // clone file from URL v1 - router.POST("/api/pin/:cid", middleware.IsAuthenticated, upload.IPFSCloneHandler(peer)) // clone file from IPFS - router.POST("/api/files", middleware.IsAuthenticated, upload.Handler) // upload file - router.GET("/api/files", middleware.IsAuthenticated, middleware.Paginate, items.JsonHandler) // list my files - router.DELETE("/api/files/:id", middleware.IsAuthenticated, upload.DeleteHandler) // delete my files + router.POST("/api/pin/:cid", middleware.IsAuthenticated, upload.IPFSCloneHandler(kuboClient)) // clone file from IPFS + router.POST("/api/files", middleware.IsAuthenticated, upload.Handler) // upload file + router.GET("/api/files", middleware.IsAuthenticated, middleware.Paginate, items.JsonHandler) // list my files + router.DELETE("/api/files/:id", middleware.IsAuthenticated, upload.DeleteHandler) // delete my files router.POST("/api/v1/files", middleware.IsAuthenticated, upload.V1Handler) // upload files with v1 @@ -97,6 +101,22 @@ func New(auth *authenticator.Authenticator, db *sql.DB, wallet *wallet.Wallet, q router.GET("/api/accounts/usage", middleware.IsAuthenticated, payment.UsageHandler) // account usage router.GET("/api/accounts/id", middleware.IsAuthenticated, payment.IdHandler) // account creation + // Add OPTIONS handlers for CORS preflight requests + router.OPTIONS("/api/accounts", func(c *gin.Context) { + c.Header("Access-Control-Allow-Origin", "*") + c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") + c.Header("Access-Control-Allow-Headers", "Authorization, Content-Type") + c.Header("Access-Control-Allow-Credentials", "true") + c.Status(http.StatusOK) + }) + router.OPTIONS("/api/accounts/usage", func(c *gin.Context) { + c.Header("Access-Control-Allow-Origin", "*") + c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") + c.Header("Access-Control-Allow-Headers", "Authorization, Content-Type") + c.Header("Access-Control-Allow-Credentials", "true") + c.Status(http.StatusOK) + }) + router.POST("/api/collections/:name", middleware.IsAuthenticated, collections.NewCollectionHandler) // create new collection router.GET("/api/collections", middleware.IsAuthenticated, middleware.Paginate, collections.ListHandler) // list my collections router.DELETE("/api/collections/:id", middleware.IsAuthenticated, collections.DeleteCollectionHandler) // delete my collections diff --git a/utils/files.go b/utils/files.go index 81cc67a..334e730 100644 --- a/utils/files.go +++ b/utils/files.go @@ -19,6 +19,7 @@ func GetFilesByCollection(db *sql.DB, collectionId int64) ([]types.Items, error) if err != nil { return nil, err } + // nolint:errcheck defer rows.Close() files := make([]types.Items, 0) @@ -45,6 +46,7 @@ func GetCollectionRefsByCollection(db *sql.DB, collectionId int64) ([]types.Coll if err != nil { return nil, err } + // nolint:errcheck defer rows.Close() cids := make([]types.Collection, 0) diff --git a/utils/kubo.go b/utils/kubo.go new file mode 100644 index 0000000..e399871 --- /dev/null +++ b/utils/kubo.go @@ -0,0 +1,312 @@ +package utils + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "mime/multipart" + "net/http" + "net/url" + "strings" + "time" + + "github.com/rs/zerolog/log" +) + +// KuboClient represents a client for interacting with IPFS Kubo node +type KuboClient struct { + apiURL string + gatewayURL string + client *http.Client +} + +// KuboResponse represents a response from Kubo API +type KuboResponse struct { + Hash string `json:"Hash"` + Name string `json:"Name"` + Size string `json:"Size"` +} + +// KuboError represents an error response from Kubo API +type KuboError struct { + Message string `json:"Message"` + Code int `json:"Code"` +} + +// KuboPinResponse represents a pin response from Kubo API +type KuboPinResponse struct { + Pins []string `json:"Pins"` +} + +// NewKuboClient creates a new Kubo client +func NewKuboClient(apiURL, gatewayURL string) *KuboClient { + return &KuboClient{ + apiURL: strings.TrimSuffix(apiURL, "/"), + gatewayURL: strings.TrimSuffix(gatewayURL, "/"), + client: &http.Client{ + Timeout: 120 * time.Second, + }, + } +} + +// AddFile adds a file to IPFS via Kubo API +func (k *KuboClient) AddFile(ctx context.Context, data io.Reader, filename string) (*KuboResponse, error) { + apiURL := k.apiURL + "/api/v0/add" + + var buf bytes.Buffer + writer := multipart.NewWriter(&buf) + + // Add the file + part, err := writer.CreateFormFile("file", filename) + if err != nil { + return nil, fmt.Errorf("failed to create form file: %w", err) + } + + _, err = io.Copy(part, data) + if err != nil { + return nil, fmt.Errorf("failed to copy file data: %w", err) + } + + // nolint:errcheck + writer.Close() + + req, err := http.NewRequestWithContext(ctx, "POST", apiURL, &buf) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Content-Type", writer.FormDataContentType()) + + resp, err := k.client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to send request: %w", err) + } + // nolint:errcheck + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("kubo API error: %d - %s", resp.StatusCode, string(body)) + } + + var result KuboResponse + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, fmt.Errorf("failed to decode response: %w", err) + } + + return &result, nil +} + +// PinCID pins a CID in IPFS +func (k *KuboClient) PinCID(ctx context.Context, cidStr string) error { + apiURL := k.apiURL + "/api/v0/pin/add" + + // Build the URL with query parameters + apiURLWithParams := apiURL + "?arg=" + url.QueryEscape(cidStr) + "&recursive=true" + + req, err := http.NewRequestWithContext(ctx, "POST", apiURLWithParams, nil) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + resp, err := k.client.Do(req) + if err != nil { + return fmt.Errorf("failed to send request: %w", err) + } + // nolint:errcheck + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("kubo pin API error: %d - %s", resp.StatusCode, string(body)) + } + + var result KuboPinResponse + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return fmt.Errorf("failed to decode pin response: %w", err) + } + + log.Info().Msgf("Successfully pinned CID: %s", cidStr) + return nil +} + +// GetFile retrieves a file from IPFS via Kubo API +func (k *KuboClient) GetFile(ctx context.Context, cidStr string) (io.ReadCloser, error) { + apiURL := k.apiURL + "/api/v0/cat" + + params := url.Values{} + params.Set("arg", cidStr) + + req, err := http.NewRequestWithContext(ctx, "POST", apiURL, strings.NewReader(params.Encode())) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + resp, err := k.client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to send request: %w", err) + } + + if resp.StatusCode != http.StatusOK { + // nolint:errcheck + resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("kubo cat API error: %d - %s", resp.StatusCode, string(body)) + } + + return resp.Body, nil +} + +// GetFileInfo gets information about a file from IPFS +func (k *KuboClient) GetFileInfo(ctx context.Context, cidStr string) (*KuboResponse, error) { + apiURL := k.apiURL + "/api/v0/object/stat" + + params := url.Values{} + params.Set("arg", cidStr) + + req, err := http.NewRequestWithContext(ctx, "POST", apiURL, strings.NewReader(params.Encode())) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + resp, err := k.client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to send request: %w", err) + } + // nolint:errcheck + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("kubo object stat API error: %d - %s", resp.StatusCode, string(body)) + } + + var result KuboResponse + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, fmt.Errorf("failed to decode object stat response: %w", err) + } + + return &result, nil +} + +// IsDirectory checks if a CID represents a directory +func (k *KuboClient) IsDirectory(ctx context.Context, cidStr string) (bool, error) { + apiURL := k.apiURL + "/api/v0/ls" + + params := url.Values{} + params.Set("arg", cidStr) + + req, err := http.NewRequestWithContext(ctx, "POST", apiURL, strings.NewReader(params.Encode())) + if err != nil { + return false, fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + resp, err := k.client.Do(req) + if err != nil { + return false, fmt.Errorf("failed to send request: %w", err) + } + // nolint:errcheck + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + // If it's not a directory, the ls command will fail + return false, nil + } + + // If we get here, it's likely a directory + return true, nil +} + +// GetDirectoryContents gets the contents of a directory +func (k *KuboClient) GetDirectoryContents(ctx context.Context, cidStr string) ([]KuboResponse, error) { + apiURL := k.apiURL + "/api/v0/ls" + + params := url.Values{} + params.Set("arg", cidStr) + + req, err := http.NewRequestWithContext(ctx, "POST", apiURL, strings.NewReader(params.Encode())) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + resp, err := k.client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to send request: %w", err) + } + // nolint:errcheck + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("kubo ls API error: %d - %s", resp.StatusCode, string(body)) + } + + // Parse the response - this is a bit complex as Kubo returns a specific format + var result struct { + Objects []struct { + Hash string `json:"Hash"` + Links []struct { + Name string `json:"Name"` + Hash string `json:"Hash"` + Size uint64 `json:"Size"` + } `json:"Links"` + } `json:"Objects"` + } + + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, fmt.Errorf("failed to decode ls response: %w", err) + } + + if len(result.Objects) == 0 { + return nil, fmt.Errorf("no objects found") + } + + var contents []KuboResponse + for _, link := range result.Objects[0].Links { + contents = append(contents, KuboResponse{ + Hash: link.Hash, + Name: link.Name, + Size: fmt.Sprintf("%d", link.Size), + }) + } + + return contents, nil +} + +// GetGatewayURL returns the gateway URL for a CID +func (k *KuboClient) GetGatewayURL(cidStr string) string { + return k.gatewayURL + "/ipfs/" + cidStr +} + +// HealthCheck checks if the Kubo node is healthy +func (k *KuboClient) HealthCheck(ctx context.Context) error { + apiURL := k.apiURL + "/api/v0/version" + + req, err := http.NewRequestWithContext(ctx, "POST", apiURL, nil) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + resp, err := k.client.Do(req) + if err != nil { + return fmt.Errorf("failed to send request: %w", err) + } + // nolint:errcheck + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("kubo health check failed: %d", resp.StatusCode) + } + + return nil +} diff --git a/utils/usage.go b/utils/usage.go index 3a7931a..f759861 100644 --- a/utils/usage.go +++ b/utils/usage.go @@ -14,6 +14,7 @@ func GetUsage(db *sql.DB, sub string) (*Usage, error) { if err != nil { return nil, err } + // nolint:errcheck defer rows.Close() var stripeID string if rows.Next() { diff --git a/web/app/items/items.go b/web/app/items/items.go index 550e1a8..598f12a 100644 --- a/web/app/items/items.go +++ b/web/app/items/items.go @@ -26,6 +26,7 @@ func Handler(ctx *gin.Context) { ctx.JSON(http.StatusInternalServerError, gin.H{"message": fmt.Sprintf("query error: %s", err.Error())}) return } + // nolint:errcheck defer rows.Close() var items []struct { diff --git a/web/app/items/jitems.go b/web/app/items/jitems.go index 8e224cd..6ee7659 100644 --- a/web/app/items/jitems.go +++ b/web/app/items/jitems.go @@ -57,6 +57,8 @@ func JsonHandler(ctx *gin.Context) { return } rows = r + + // nolint:errcheck defer r.Close() } else { // Fetch the paginated list of files @@ -66,6 +68,7 @@ func JsonHandler(ctx *gin.Context) { return } rows = r + // nolint:errcheck defer r.Close() } @@ -142,6 +145,7 @@ func CollectionHandler(ctx *gin.Context) { }) return } + // nolint:errcheck defer rows.Close() files := make([]types.Items, 0) diff --git a/web/app/keys/keys.go b/web/app/keys/keys.go index 4174b37..98f17f6 100644 --- a/web/app/keys/keys.go +++ b/web/app/keys/keys.go @@ -6,10 +6,11 @@ import ( "net/http" "time" - "github.com/rs/zerolog/log" "jackalnft/platform/middleware" "jackalnft/types" + "github.com/rs/zerolog/log" + "github.com/gin-gonic/gin" ) @@ -103,6 +104,7 @@ func ListKeyHandler(ctx *gin.Context) { ctx.JSON(http.StatusInternalServerError, gin.H{"message": fmt.Sprintf("query error: %s", err.Error())}) return } + // nolint:errcheck defer rows.Close() keys := make([]Key, 0) diff --git a/web/app/upload/upload.go b/web/app/upload/upload.go index 2789021..208bd59 100644 --- a/web/app/upload/upload.go +++ b/web/app/upload/upload.go @@ -8,14 +8,11 @@ import ( "io" "net/http" "path/filepath" - "strings" - - "github.com/ipfs/boxo/ipld/unixfs" "jackalnft/platform/collections" - ipfslite "github.com/hsanjuan/ipfs-lite" "github.com/ipfs/go-cid" + "github.com/rs/zerolog/log" "jackalnft/jackal/uploader" @@ -61,6 +58,7 @@ func Handler(ctx *gin.Context) { ctx.JSON(http.StatusInternalServerError, gin.H{"message": "cannot open file"}) return } + // nolint:errcheck defer f.Close() // Retrieve the file name @@ -73,11 +71,15 @@ func Handler(ctx *gin.Context) { return } - cid, root, _, err := uploader.ProcessFile(ctx, filename, data, false, "") + // Get Kubo client from context and create uploader + kuboClient := ctx.MustGet("kubo").(*utils.KuboClient) + kuboUploader := uploader.NewKuboUploader(kuboClient) + cid, root, _, err := kuboUploader.ProcessFileWithKubo(ctx, filename, data, false, "") if err != nil { - fmt.Println(err.Error()) + log.Error().Err(err).Msg("Failed to process file") ctx.JSON(http.StatusInternalServerError, gin.H{ "message": "could not process file", + "error": err.Error(), }) return } @@ -86,7 +88,7 @@ func Handler(ctx *gin.Context) { // Insert file metadata into the database _, err = db.Exec("INSERT INTO files (user_id, file_name, cid, root, size) VALUES ($1, $2, $3, $4, $5)", sub, filename, cid, merkle, file.Size) if err != nil { - fmt.Println(err.Error()) + log.Error().Err(err).Msg("Failed to insert file into database") ctx.JSON(http.StatusInternalServerError, gin.H{"message": "failed to save record"}) return } @@ -143,6 +145,7 @@ func V1Handler(ctx *gin.Context) { ctx.JSON(http.StatusInternalServerError, gin.H{"message": "cannot open file"}) return } + // nolint:errcheck defer f.Close() // Retrieve the file name @@ -210,6 +213,7 @@ func V1CloneHandler(ctx *gin.Context) { ctx.JSON(http.StatusBadRequest, gin.H{"message": fmt.Errorf("could not download contents from link : %w", err).Error()}) return } + // nolint:errcheck defer resp.Body.Close() if resp.StatusCode != http.StatusOK { @@ -280,6 +284,7 @@ func CloneHandler(ctx *gin.Context) { ctx.JSON(http.StatusBadRequest, gin.H{"message": fmt.Errorf("could not download contents from link : %w", err).Error()}) return } + // nolint:errcheck defer resp.Body.Close() if resp.StatusCode != http.StatusOK { @@ -327,9 +332,8 @@ func CloneHandler(ctx *gin.Context) { } // IPFSCloneHandler gets a file from IPFS and saves it -func IPFSCloneHandler(p *ipfslite.Peer) func(ctx *gin.Context) { +func IPFSCloneHandler(kuboClient *utils.KuboClient) func(ctx *gin.Context) { return func(ctx *gin.Context) { - peer := p sub, exists := ctx.Get(types.UserIDKey) if !exists { ctx.JSON(http.StatusUnauthorized, gin.H{"message": "not authorized"}) @@ -346,7 +350,7 @@ func IPFSCloneHandler(p *ipfslite.Peer) func(ctx *gin.Context) { ctx.Status(http.StatusAccepted) - err = saveIPFS(ctx, peer, c.String(), c, sub.(string), 0) + err = saveIPFS(ctx, kuboClient, c.String(), c, sub.(string), 0) if err != nil { fmt.Println(err) return @@ -354,34 +358,17 @@ func IPFSCloneHandler(p *ipfslite.Peer) func(ctx *gin.Context) { } } -func saveIPFS(ctx *gin.Context, peer *ipfslite.Peer, fileName string, c cid.Cid, user string, parentId int64) error { - node, err := peer.Get(ctx, c) +func saveIPFS(ctx *gin.Context, kuboClient *utils.KuboClient, fileName string, c cid.Cid, user string, parentId int64) error { + // Check if it's a directory using Kubo client + isDir, err := kuboClient.IsDirectory(ctx, c.String()) if err != nil { fmt.Println(err) - return err + // Continue as file, we'll handle errors later } - db := ctx.MustGet("db").(*sql.DB) - folder := false - folderExtract, err := unixfs.ExtractFSNode(node) - if err == nil { - folder = folderExtract.Type() == unixfs.TDirectory - fmt.Printf("%s (%s) is a folder? %t\n", c.String(), folderExtract.Type().String(), folder) - } else { - fmt.Println(err) - } - - dataBuf, err := peer.GetFile(ctx, c) - if err != nil { - fmt.Println(err) - if strings.Contains(err.Error(), "directory") { - folder = true - } else { - return err - } - } + folder := isDir + db := ctx.MustGet("db").(*sql.DB) - lll := node.Links() if folder { // it's a folder var id int64 err := db.QueryRow("INSERT INTO collections (collection_name, user_id) VALUES ($1, $2) RETURNING id", c.String(), user).Scan(&id) @@ -391,7 +378,6 @@ func saveIPFS(ctx *gin.Context, peer *ipfslite.Peer, fileName string, c cid.Cid, } if parentId > 0 { - query := ` INSERT INTO collection_refs (collection_id, ref_id) SELECT $1, $2 @@ -405,11 +391,35 @@ func saveIPFS(ctx *gin.Context, peer *ipfslite.Peer, fileName string, c cid.Cid, } } - for _, link := range lll { - fmt.Println(link.Name, link.Cid) - err = saveIPFS(ctx, peer, link.Name, link.Cid, user, id) + // Get directory contents using Kubo client + contents, err := kuboClient.GetDirectoryContents(ctx, c.String()) + if err != nil { + return fmt.Errorf("failed to get directory contents: %w", err) + } + + // Process each item in the directory + for _, item := range contents { + // Check if item is a directory + isItemDir, err := kuboClient.IsDirectory(ctx, item.Hash) if err != nil { - return err + fmt.Printf("Error checking if item is directory: %v\n", err) + continue + } + + if isItemDir { + // Recursively process subdirectory + err = saveIPFS(ctx, kuboClient, item.Name, cid.MustParse(item.Hash), user, id) + if err != nil { + fmt.Printf("Error processing subdirectory %s: %v\n", item.Name, err) + continue + } + } else { + // Process file + err = saveFileFromIPFS(ctx, kuboClient, item.Hash, user) + if err != nil { + fmt.Printf("Error processing file %s: %v\n", item.Name, err) + continue + } } } @@ -417,7 +427,15 @@ func saveIPFS(ctx *gin.Context, peer *ipfslite.Peer, fileName string, c cid.Cid, return nil } - data, err := io.ReadAll(dataBuf) + // Get file data using Kubo client + fileData, err := kuboClient.GetFile(ctx, c.String()) + if err != nil { + return fmt.Errorf("failed to get file data: %w", err) + } + // nolint:errcheck + defer fileData.Close() + + data, err := io.ReadAll(fileData) if err != nil { return err } @@ -454,3 +472,53 @@ func saveIPFS(ctx *gin.Context, peer *ipfslite.Peer, fileName string, c cid.Cid, return nil } + +// saveFileFromIPFS saves a file from IPFS to the database using Kubo client +func saveFileFromIPFS(ctx *gin.Context, kuboClient *utils.KuboClient, cidStr string, userID string) error { + db := ctx.MustGet("db").(*sql.DB) + + // Get file info + fileInfo, err := kuboClient.GetFileInfo(ctx, cidStr) + if err != nil { + return fmt.Errorf("failed to get file info: %w", err) + } + + // Get file data + fileData, err := kuboClient.GetFile(ctx, cidStr) + if err != nil { + return fmt.Errorf("failed to get file data: %w", err) + } + // nolint:errcheck + defer fileData.Close() + + // Read file data + data, err := io.ReadAll(fileData) + if err != nil { + return fmt.Errorf("failed to read file data: %w", err) + } + + // Generate CID and process file + fileName := fileInfo.Name + if fileName == "" { + fileName = cidStr + } + + // Use the existing ProcessFile function to handle the blockchain posting + cid, root, size, err := uploader.ProcessFile(ctx, fileName, data, false, cidStr) + if err != nil { + return fmt.Errorf("failed to process file: %w", err) + } + + // Convert root to hex string + merkle := hex.EncodeToString(root) + + // Insert file metadata into the database + _, err = db.Exec("INSERT INTO files (user_id, file_name, cid, root, size) VALUES ($1, $2, $3, $4, $5)", + userID, fileName, cid, merkle, size) + if err != nil { + return fmt.Errorf("failed to save file record: %w", err) + } + + fmt.Printf("Successfully saved file from IPFS: %s (CID: %s, Size: %d)\n", fileName, cid, size) + return nil +} From a78ff7efa1453128e746a54c10381fe87763077b Mon Sep 17 00:00:00 2001 From: marston Date: Mon, 6 Oct 2025 08:57:08 -0400 Subject: [PATCH 4/4] makefile --- Makefile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 5bd52ce..b59d993 100644 --- a/Makefile +++ b/Makefile @@ -68,7 +68,7 @@ test-unit: @go test -mod=readonly -v -coverprofile coverage.txt ./... .PHONY: test-unit -.PHONY: help build up down logs clean restart shell-app shell-db shell-ipfs test +.PHONY: help build up down logs clean restart shell-app shell-db shell-ipfs test docker # Default target help: @@ -171,3 +171,5 @@ init-ipfs: docker compose exec ipfs ipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin '["*"]' docker compose exec ipfs ipfs config --json API.HTTPHeaders.Access-Control-Allow-Methods '["PUT", "POST", "GET"]' docker compose exec ipfs ipfs config --json API.HTTPHeaders.Access-Control-Allow-Headers '["Authorization"]' + +docker: build up \ No newline at end of file