feat: add API for adding watermark image to video, with tests and read-me file
This commit is contained in:
parent
6694cf0170
commit
547c79123d
85
apps/demo-nextjs-app-router/api_scripts/hello_test.py
Normal file
85
apps/demo-nextjs-app-router/api_scripts/hello_test.py
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Base URL for the API
|
||||||
|
BASE_URL = "http://localhost:4200/api/script"
|
||||||
|
|
||||||
|
# Arguments to pass to the script
|
||||||
|
ARG1 = "hello"
|
||||||
|
ARG2 = "world"
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("Starting script execution...")
|
||||||
|
|
||||||
|
# Start script execution and get token
|
||||||
|
try:
|
||||||
|
response = requests.post(
|
||||||
|
BASE_URL,
|
||||||
|
json={
|
||||||
|
"scriptPath": "/tmp/api_scripts/hello_test.sh",
|
||||||
|
"args": [ARG1, ARG2]
|
||||||
|
},
|
||||||
|
headers={"Content-Type": "application/json"}
|
||||||
|
)
|
||||||
|
response.raise_for_status() # Raise exception for non-200 status codes
|
||||||
|
token_data = response.json()
|
||||||
|
token = token_data.get("token")
|
||||||
|
|
||||||
|
if not token:
|
||||||
|
print(f"Failed to get token. Response: {response.text}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(f"Received token: {token}")
|
||||||
|
print("Polling for results...")
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
print(f"Error starting script execution: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Poll for results with timeout
|
||||||
|
MAX_ATTEMPTS = 30
|
||||||
|
SLEEP_SECONDS = 1
|
||||||
|
status = "pending"
|
||||||
|
|
||||||
|
for attempt in range(1, MAX_ATTEMPTS + 1):
|
||||||
|
if status not in ["pending", "running"]:
|
||||||
|
break
|
||||||
|
|
||||||
|
print(f"Polling attempt {attempt} of {MAX_ATTEMPTS}...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
result_response = requests.get(f"{BASE_URL}?token={token}")
|
||||||
|
result_response.raise_for_status()
|
||||||
|
result_data = result_response.json()
|
||||||
|
status = result_data.get("status")
|
||||||
|
|
||||||
|
if status in ["pending", "running"]:
|
||||||
|
print(f"Script is still {status}. Waiting {SLEEP_SECONDS}s...")
|
||||||
|
time.sleep(SLEEP_SECONDS)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
print(f"Error polling for results: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if status in ["pending", "running"]:
|
||||||
|
print("Timeout waiting for script to complete")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Print results
|
||||||
|
print(f"Script execution completed with status: {status}")
|
||||||
|
print(f"Result: {json.dumps(result_data, indent=2)}")
|
||||||
|
|
||||||
|
# Pretty print stdout if available
|
||||||
|
if result_data.get("result", {}).get("stdout"):
|
||||||
|
stdout = result_data["result"]["stdout"]
|
||||||
|
print("\nScript output:")
|
||||||
|
print(stdout)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
131
apps/demo-nextjs-app-router/api_scripts/video_add_watermark.md
Normal file
131
apps/demo-nextjs-app-router/api_scripts/video_add_watermark.md
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
# Video Watermark API Usage Guide
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Video Watermark API allows you to add watermarks to video files asynchronously. The process involves two steps:
|
||||||
|
|
||||||
|
1. Submit a watermark job and receive a token
|
||||||
|
2. Use the token to check the job status and get the result
|
||||||
|
|
||||||
|
## Base URL
|
||||||
|
|
||||||
|
```
|
||||||
|
https://animator-gg-api.ca2324.servep2p.com:8443
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 1: Submit a Video Watermark Job
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "https://animator-gg-api.ca2324.servep2p.com:8443/api/script" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"scriptPath": "/tmp/api_scripts/video_add_watermark.sh",
|
||||||
|
"args": [
|
||||||
|
"https://example.com/input.mp4",
|
||||||
|
"https://example.com/watermark.png",
|
||||||
|
"30",
|
||||||
|
"30"
|
||||||
|
]
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
- `scriptPath`: Must be video_add_watermark.sh
|
||||||
|
- `args`: Array containing:
|
||||||
|
- Input video URL (required)
|
||||||
|
- Watermark image URL (required)
|
||||||
|
- X position in pixels (required)
|
||||||
|
- Y position in pixels (required)
|
||||||
|
- Width for watermark resize (optional, 0 for no resize)
|
||||||
|
- Height for watermark resize (optional, 0 for no resize)
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"token": "550e8400-e29b-41d4-a716-446655440000"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 2: Check Job Status and Get Result
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X GET "https://animator-gg-api.ca2324.servep2p.com:8443/api/script?token=550e8400-e29b-41d4-a716-446655440000"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "completed",
|
||||||
|
"result": {
|
||||||
|
"success": true,
|
||||||
|
"stdout": "{\n \"input_video_url\": \"https://example.com/input.mp4\",\n \"watermark_png_url\": \"https://example.com/watermark.png\", \n \"pos_x\": 30,\n \"pos_y\": 30,\n \"size_w\": 0,\n \"size_h\": 0,\n \"output_file_name\": \"/var/www/html/watermarked_videos/bcc55e77-f1b4-472c-8a1b-0f3c852bd73a.mp4\",\n \"output_file_url\": \"https://example.com/watermarked_videos/bcc55e77-f1b4-472c-8a1b-0f3c852bd73a.mp4\"\n}",
|
||||||
|
"stderr": "..."
|
||||||
|
},
|
||||||
|
"timestamp": 1743991194237
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
When the job is complete, the `stdout` field will contain a JSON string with the output video URL. You'll need to parse this string to get the actual URL.
|
||||||
|
|
||||||
|
## Example: Complete Process
|
||||||
|
|
||||||
|
1. **Submit the job:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
TOKEN=$(curl -s -X POST "https://animator-gg-api.ca2324.servep2p.com:8443/api/script" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"scriptPath": "/tmp/api_scripts/video_add_watermark.sh",
|
||||||
|
"args": [
|
||||||
|
"https://ca2324.ddns.net:8443/jerry/_seed847064922.mp4",
|
||||||
|
"https://ca2324.ddns.net:8443/jerry/0a97d84a70fec3991cf05ee99ceb6469.png",
|
||||||
|
"30",
|
||||||
|
"30"
|
||||||
|
]
|
||||||
|
}' | jq -r '.token')
|
||||||
|
|
||||||
|
echo "Job submitted with token: $TOKEN"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Check status until complete:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
while true; do
|
||||||
|
RESULT=$(curl -s -X GET "https://animator-gg-api.ca2324.servep2p.com:8443/api/script?token=$TOKEN")
|
||||||
|
STATUS=$(echo $RESULT | jq -r '.status')
|
||||||
|
echo "Status: $STATUS"
|
||||||
|
|
||||||
|
if [ "$STATUS" = "completed" ] || [ "$STATUS" = "failed" ]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
# Extract the watermarked video URL
|
||||||
|
if [ "$STATUS" = "completed" ]; then
|
||||||
|
OUTPUT_JSON=$(echo $RESULT | jq -r '.result.stdout')
|
||||||
|
OUTPUT_URL=$(echo $OUTPUT_JSON | jq -r '.output_file_url')
|
||||||
|
echo "Watermarked video URL: $OUTPUT_URL"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
## Status Codes
|
||||||
|
|
||||||
|
- `pending`: Job is waiting in the queue
|
||||||
|
- `running`: Job is currently processing
|
||||||
|
- `completed`: Job finished successfully
|
||||||
|
- `failed`: Job failed to complete
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Results are stored for 24 hours after completion
|
||||||
|
- Processing large videos may take several minutes
|
||||||
|
- The API is rate-limited to 2 concurrent jobs
|
||||||
101
apps/demo-nextjs-app-router/api_scripts/video_add_watermark.py
Normal file
101
apps/demo-nextjs-app-router/api_scripts/video_add_watermark.py
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
|
||||||
|
#bash video_add_watermark.sh "$VID" "$PNG" 30 30
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Base URL for the API
|
||||||
|
BASE_URL = "https://animator-gg-api.ca2324.servep2p.com:8443/api/script"
|
||||||
|
|
||||||
|
# Arguments to pass to the script
|
||||||
|
|
||||||
|
VID="https://ca2324.ddns.net:8443/jerry/_seed847064922.mp4"
|
||||||
|
PNG="https://ca2324.ddns.net:8443/jerry/0a97d84a70fec3991cf05ee99ceb6469.png"
|
||||||
|
|
||||||
|
SAMPLE_OUPUT = """
|
||||||
|
{
|
||||||
|
"status": "completed",
|
||||||
|
"result": {
|
||||||
|
"success": true,
|
||||||
|
"stdout": "{\n \"input_video_url\": \"https://ca2324.ddns.net:8443/jerry/_seed847064922.mp4\",\n \"watermark_png_url\": \"https://ca2324.ddns.net:8443/jerry/0a97d84a70fec3991cf05ee99ceb6469.png\", \n \"pos_x\": 30,\n \"pos_y\": 30,\n \"size_w\": 0,\n \"size_h\": 0,\n \"output_file_name\": \"/var/www/html/watermarked_videos//bcc55e77-f1b4-472c-8a1b-0f3c852bd73a.mp4\",\n \"output_file_url\": \"https://ca2324.ddns.net:8443/watermarked_videos/bcc55e77-f1b4-472c-8a1b-0f3c852bd73a.mp4\"\n}",
|
||||||
|
"stderr": "x265 [info]: HEVC encoder version 4.1+1-1d117be\nx265 [info]: build info [Linux][GCC 11.4.0][64 bit] 8bit+10bit+12bit\nx265 [info]: using cpu capabilities: MMX2 SSE2Fast LZCNT SSSE3 SSE4.2 AVX FMA3 BMI2 AVX2\nx265 [info]: Main profile, Level-4 (Main tier)\nx265 [info]: Thread pool created using 56 threads\nx265 [info]: Slices : 1\nx265 [info]: frame threads / pool features : 5 / wpp(25 rows)\nx265 [info]: Coding QT: max CU size, min CU size : 64 / 8\nx265 [info]: Residual QT: max TU size, max depth : 32 / 1 inter / 1 intra\nx265 [info]: ME / range / subpel / merge : hex / 57 / 1 / 2\nx265 [info]: Keyframe min / max / scenecut / bias : 24 / 250 / 40 / 5.00 \nx265 [info]: Lookahead / bframes / badapt : 15 / 4 / 0\nx265 [info]: b-pyramid / weightp / weightb : 1 / 1 / 0\nx265 [info]: References / ref-limit cu / depth : 2 / on / on\nx265 [info]: AQ: mode / str / qg-size / cu-tree : 2 / 1.0 / 32 / 1\nx265 [info]: Rate Control / qCompress : CRF-24.0 / 0.60\nx265 [info]: tools: rd=2 psy-rd=2.00 early-skip rskip mode=1 signhide tmvp\nx265 [info]: tools: fast-intra strong-intra-smoothing lslices=8 deblock sao\nx265 [info]: frame I: 2, Avg QP:24.88 kb/s: 14848.22\nx265 [info]: frame P: 95, Avg QP:24.31 kb/s: 8688.46 \nx265 [info]: frame B: 384, Avg QP:29.93 kb/s: 2459.11 \nx265 [info]: Weighted P-Frames: Y:8.4% UV:5.3%\n\nencoded 481 frames in 8.94s (53.79 fps), 3740.95 kb/s, Avg QP:28.80"
|
||||||
|
},
|
||||||
|
"timestamp": 1743991194237
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("Starting script execution...")
|
||||||
|
|
||||||
|
# Start script execution and get token
|
||||||
|
try:
|
||||||
|
response = requests.post(
|
||||||
|
BASE_URL,
|
||||||
|
json={
|
||||||
|
"scriptPath": "/tmp/api_scripts/video_add_watermark.sh",
|
||||||
|
"args": [VID, PNG, 30, 30]
|
||||||
|
},
|
||||||
|
headers={"Content-Type": "application/json"}
|
||||||
|
)
|
||||||
|
response.raise_for_status() # Raise exception for non-200 status codes
|
||||||
|
token_data = response.json()
|
||||||
|
token = token_data.get("token")
|
||||||
|
|
||||||
|
if not token:
|
||||||
|
print(f"Failed to get token. Response: {response.text}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(f"Received token: {token}")
|
||||||
|
print("Polling for results...")
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
print(f"Error starting script execution: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Poll for results with timeout
|
||||||
|
MAX_ATTEMPTS = 30
|
||||||
|
SLEEP_SECONDS = 1
|
||||||
|
status = "pending"
|
||||||
|
|
||||||
|
for attempt in range(1, MAX_ATTEMPTS + 1):
|
||||||
|
if status not in ["pending", "running"]:
|
||||||
|
break
|
||||||
|
|
||||||
|
print(f"Polling attempt {attempt} of {MAX_ATTEMPTS}...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
result_response = requests.get(f"{BASE_URL}?token={token}")
|
||||||
|
result_response.raise_for_status()
|
||||||
|
result_data = result_response.json()
|
||||||
|
status = result_data.get("status")
|
||||||
|
|
||||||
|
if status in ["pending", "running"]:
|
||||||
|
print(f"Script is still {status}. Waiting {SLEEP_SECONDS}s...")
|
||||||
|
time.sleep(SLEEP_SECONDS)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
print(f"Error polling for results: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if status in ["pending", "running"]:
|
||||||
|
print("Timeout waiting for script to complete")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Print results
|
||||||
|
print(f"Script execution completed with status: {status}")
|
||||||
|
print(f"Result: {json.dumps(result_data, indent=2)}")
|
||||||
|
|
||||||
|
# Pretty print stdout if available
|
||||||
|
if result_data.get("result", {}).get("stdout"):
|
||||||
|
stdout = result_data["result"]["stdout"]
|
||||||
|
print("\nScript output:")
|
||||||
|
print(stdout)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@ -1,4 +1,112 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
echo "Script executed with args: $1 $2"
|
|
||||||
echo "Current time: $(date)"
|
output_folder="/var/www/html/watermarked_videos/"
|
||||||
echo "Current directory: $(pwd)"
|
output_base_url="https://ca2324.ddns.net:8443/watermarked_videos/"
|
||||||
|
# Input parameters
|
||||||
|
input_video_url="$1"
|
||||||
|
watermark_png_url="$2"
|
||||||
|
pos_x="${3:-10}" # Default to 10 if not provided
|
||||||
|
pos_y="${4:-10}" # Default to 10 if not provided
|
||||||
|
size_w="${5:-0}" # Default to 0 (keep aspect ratio) if not provided
|
||||||
|
size_h="${6:-0}" # Default to 0 (keep aspect ratio) if not provided
|
||||||
|
|
||||||
|
# Validate required inputs
|
||||||
|
if [ -z "$input_video_url" ] || [ -z "$watermark_png_url" ]; then
|
||||||
|
echo "Error: Missing required parameters" >&2
|
||||||
|
echo "Usage: $0 input_video_url watermark_png_url [pos_x] [pos_y] [size_w] [size_h]" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create temp directory
|
||||||
|
temp_dir="/tmp/watermark_$$"
|
||||||
|
mkdir -p "$temp_dir" || {
|
||||||
|
echo "Error: Failed to create temporary directory" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Download input video
|
||||||
|
video_filename=$(basename "$input_video_url")
|
||||||
|
local_video_path="$temp_dir/$video_filename"
|
||||||
|
if ! curl -s -L -o "$local_video_path" "$input_video_url"; then
|
||||||
|
echo "Error: Failed to download input video from $input_video_url" >&2
|
||||||
|
rm -rf "$temp_dir"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Download watermark image
|
||||||
|
watermark_filename=$(basename "$watermark_png_url")
|
||||||
|
local_watermark_path="$temp_dir/$watermark_filename"
|
||||||
|
if ! curl -s -L -o "$local_watermark_path" "$watermark_png_url"; then
|
||||||
|
echo "Error: Failed to download watermark image from $watermark_png_url" >&2
|
||||||
|
rm -rf "$temp_dir"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validate downloaded files
|
||||||
|
if [ ! -f "$local_video_path" ]; then
|
||||||
|
echo "Error: Downloaded video file not found" >&2
|
||||||
|
rm -rf "$temp_dir"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$local_watermark_path" ]; then
|
||||||
|
echo "Error: Downloaded watermark file not found" >&2
|
||||||
|
rm -rf "$temp_dir"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Generate a UUID-style output filename
|
||||||
|
output_uuid=$(uuidgen)
|
||||||
|
output_file="$temp_dir/$output_uuid.mp4"
|
||||||
|
|
||||||
|
# Prepare ffmpeg filter for watermark
|
||||||
|
if [ "$size_w" -eq 0 ] && [ "$size_h" -eq 0 ]; then
|
||||||
|
# No resizing
|
||||||
|
filter_complex="overlay=$pos_x:$pos_y"
|
||||||
|
else
|
||||||
|
# Resize watermark before overlay
|
||||||
|
filter_complex="overlay=$pos_x:$pos_y:scale=$size_w:$size_h"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Add watermark using ffmpeg and encode to HEVC (H.265)
|
||||||
|
if ! ffmpeg -y -loglevel error -i "$local_video_path" -i "$local_watermark_path" \
|
||||||
|
-filter_complex "$filter_complex" \
|
||||||
|
-c:v libx265 -tag:v hvc1 -crf 24 -preset veryfast \
|
||||||
|
-c:a copy \
|
||||||
|
-movflags +faststart \
|
||||||
|
"${output_file}" 1>&2 ; then
|
||||||
|
echo "Error: Failed to add watermark to video" >&2
|
||||||
|
rm -rf "$temp_dir"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Move output file to the output folder
|
||||||
|
if ! mv "$output_file" "$output_folder"; then
|
||||||
|
echo "Error: Failed to move output file to $output_folder" >&2
|
||||||
|
rm -rf "$temp_dir"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# Clean up temporary files
|
||||||
|
rm -rf "$temp_dir"
|
||||||
|
# Generate the final output file path
|
||||||
|
output_file="$output_folder/$output_uuid.mp4"
|
||||||
|
# Generate the final output URL
|
||||||
|
output_file_url="${output_base_url}${output_uuid}.mp4"
|
||||||
|
|
||||||
|
# Return JSON with parameters and output filename
|
||||||
|
cat << EOF
|
||||||
|
{
|
||||||
|
"input_video_url": "$input_video_url",
|
||||||
|
"watermark_png_url": "$watermark_png_url",
|
||||||
|
"pos_x": $pos_x,
|
||||||
|
"pos_y": $pos_y,
|
||||||
|
"size_w": $size_w,
|
||||||
|
"size_h": $size_h,
|
||||||
|
"output_file_name": "$output_file",
|
||||||
|
"output_file_url": "$output_file_url"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Note: We're not deleting the temp directory so that the output file remains available
|
||||||
|
|
||||||
|
exit 0
|
||||||
12
apps/demo-nextjs-app-router/app/api/script/queue/route.tsx
Normal file
12
apps/demo-nextjs-app-router/app/api/script/queue/route.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { getQueueStats } from "../../../../services/bashExecService";
|
||||||
|
|
||||||
|
// Endpoint to get queue statistics
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const stats = getQueueStats();
|
||||||
|
return NextResponse.json(stats);
|
||||||
|
} catch (error: any) {
|
||||||
|
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,9 +1,10 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import {
|
import {
|
||||||
executeScript,
|
executeScript,
|
||||||
getQueueStats,
|
getScriptExecution,
|
||||||
} from "../../../services/bashExecService";
|
} from "../../../services/bashExecService";
|
||||||
|
|
||||||
|
// Modified POST method that returns a token instead of waiting for execution result
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
@ -16,22 +17,33 @@ export async function POST(request: NextRequest) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await executeScript({
|
const token = executeScript({
|
||||||
scriptPath,
|
scriptPath,
|
||||||
args,
|
args,
|
||||||
});
|
});
|
||||||
|
|
||||||
return NextResponse.json(result);
|
return NextResponse.json({ token });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Endpoint to get queue statistics
|
// API to query execution result by token from path parameter
|
||||||
export async function GET() {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const stats = getQueueStats();
|
// Get token from the search params in the URL
|
||||||
return NextResponse.json(stats);
|
const { searchParams } = new URL(request.url);
|
||||||
|
const token = searchParams.get("token");
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Token is required as a query parameter" },
|
||||||
|
{ status: 400 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const execution = getScriptExecution(token);
|
||||||
|
return NextResponse.json(execution);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,13 @@
|
|||||||
import { execa } from "execa";
|
import { execa } from "execa";
|
||||||
import PQueue from "p-queue";
|
import PQueue from "p-queue";
|
||||||
|
import { v4 as uuidv4 } from "uuid"; // Make sure to install this package
|
||||||
|
|
||||||
// Configure the queue with a concurrency limit
|
// Configure the queue with a concurrency limit
|
||||||
const scriptQueue = new PQueue({ concurrency: 5 });
|
const scriptQueue = new PQueue({ concurrency: 2 });
|
||||||
|
|
||||||
|
// Cleanup interval (24 hours in ms)
|
||||||
|
const CLEANUP_INTERVAL = 24 * 60 * 60 * 1000;
|
||||||
|
const RESULT_EXPIRY = 24 * 60 * 60 * 1000; // 24 hours in ms
|
||||||
|
|
||||||
export type ScriptParams = {
|
export type ScriptParams = {
|
||||||
scriptPath: string;
|
scriptPath: string;
|
||||||
@ -17,37 +22,76 @@ export type ScriptResult = {
|
|||||||
error?: string;
|
error?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ScriptQueueState = {
|
||||||
|
status: "pending" | "running" | "completed" | "failed";
|
||||||
|
result?: ScriptResult;
|
||||||
|
timestamp: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Store execution results by token
|
||||||
|
const executionResults = new Map<string, ScriptQueueState>();
|
||||||
/**
|
/**
|
||||||
* Executes a bash script with queue management
|
* Clean up expired execution results
|
||||||
*/
|
*/
|
||||||
export async function executeScript(
|
function cleanupExpiredResults() {
|
||||||
params: ScriptParams,
|
const now = Date.now();
|
||||||
): Promise<ScriptResult> {
|
for (const [token, data] of executionResults.entries()) {
|
||||||
// Add the script execution to the queue
|
if (now - data.timestamp > RESULT_EXPIRY) {
|
||||||
|
executionResults.delete(token);
|
||||||
// hardcoded the allowed script path for security reasons
|
}
|
||||||
var allowedScriptPaths = [
|
|
||||||
"/tmp/api_scripts/hello_test.sh",
|
|
||||||
"/tmp/api_scripts/video_add_watermark.sh",
|
|
||||||
];
|
|
||||||
|
|
||||||
if (!allowedScriptPaths.includes(params.scriptPath)) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
stdout: "",
|
|
||||||
stderr: "",
|
|
||||||
error: "Script path is not allowed",
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const result = await scriptQueue.add(async () => {
|
// Set up periodic cleanup
|
||||||
|
setInterval(cleanupExpiredResults, CLEANUP_INTERVAL);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a bash script with queue management and returns a token immediately
|
||||||
|
*/
|
||||||
|
export function executeScript(params: ScriptParams): string {
|
||||||
|
const token = uuidv4();
|
||||||
|
|
||||||
|
// Initialize result as pending with current timestamp
|
||||||
|
executionResults.set(token, {
|
||||||
|
status: "pending",
|
||||||
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add the script execution to the queue
|
||||||
|
scriptQueue.add(async () => {
|
||||||
try {
|
try {
|
||||||
|
// hardcoded the allowed script path for security reasons
|
||||||
|
var allowedScriptPaths = [
|
||||||
|
"/tmp/api_scripts/hello_test.sh",
|
||||||
|
"/tmp/api_scripts/video_add_watermark.sh",
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!allowedScriptPaths.includes(params.scriptPath)) {
|
||||||
|
executionResults.set(token, {
|
||||||
|
status: "failed",
|
||||||
|
result: {
|
||||||
|
success: false,
|
||||||
|
stdout: "",
|
||||||
|
stderr: "",
|
||||||
|
error: "Script path is not allowed",
|
||||||
|
},
|
||||||
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update status to running
|
||||||
|
executionResults.set(token, {
|
||||||
|
status: "running",
|
||||||
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
|
|
||||||
// Execute the bash script
|
// Execute the bash script
|
||||||
const { stdout, stderr } = await execa(
|
const { stdout, stderr } = await execa(
|
||||||
"bash",
|
"bash",
|
||||||
[params.scriptPath, ...(params.args || [])],
|
[params.scriptPath, ...(params.args || [])],
|
||||||
{
|
{
|
||||||
buffer: true, // Changed to true to ensure we get the complete output
|
buffer: true, // Ensure we get the complete output
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -56,23 +100,60 @@ export async function executeScript(
|
|||||||
params.onProgress(stdout.toString());
|
params.onProgress(stdout.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
// Store successful result
|
||||||
success: true,
|
executionResults.set(token, {
|
||||||
stdout: stdout ? stdout.toString() : "",
|
status: "completed",
|
||||||
stderr: stderr ? stderr.toString() : "",
|
result: {
|
||||||
};
|
success: true,
|
||||||
|
stdout: stdout ? stdout.toString() : "",
|
||||||
|
stderr: stderr ? stderr.toString() : "",
|
||||||
|
},
|
||||||
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// Handle script execution errors
|
// Handle script execution errors
|
||||||
return {
|
executionResults.set(token, {
|
||||||
success: false,
|
status: "failed",
|
||||||
stdout: error.stdout ? error.stdout.toString() : "",
|
result: {
|
||||||
stderr: error.stderr ? error.stderr.toString() : "",
|
success: false,
|
||||||
error: error.message || "Failed to execute script",
|
stdout: error.stdout ? error.stdout.toString() : "",
|
||||||
};
|
stderr: error.stderr ? error.stderr.toString() : "",
|
||||||
|
error: error.message || "Failed to execute script",
|
||||||
|
},
|
||||||
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return result as ScriptResult;
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query the status and result of a script execution
|
||||||
|
* @param token The token returned by executeScript
|
||||||
|
* @returns An object containing execution status and result (if available)
|
||||||
|
*/
|
||||||
|
export function getScriptExecution(token: string): ScriptQueueState {
|
||||||
|
const execution = executionResults.get(token);
|
||||||
|
|
||||||
|
if (!execution) {
|
||||||
|
return {
|
||||||
|
status: "failed",
|
||||||
|
result: {
|
||||||
|
success: false,
|
||||||
|
stdout: "",
|
||||||
|
stderr: "",
|
||||||
|
error: "Invalid execution token or result expired",
|
||||||
|
},
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: execution.status,
|
||||||
|
result: execution.result,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get current queue size and pending tasks
|
// Get current queue size and pending tasks
|
||||||
@ -81,6 +162,7 @@ export function getQueueStats() {
|
|||||||
size: scriptQueue.size,
|
size: scriptQueue.size,
|
||||||
pending: scriptQueue.pending,
|
pending: scriptQueue.pending,
|
||||||
isPaused: scriptQueue.isPaused,
|
isPaused: scriptQueue.isPaused,
|
||||||
|
activeExecutions: executionResults.size,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
34
package-lock.json
generated
34
package-lock.json
generated
@ -42,7 +42,8 @@
|
|||||||
"regenerator-runtime": "0.13.7",
|
"regenerator-runtime": "0.13.7",
|
||||||
"robot3": "^0.4.1",
|
"robot3": "^0.4.1",
|
||||||
"ts-morph": "^17.0.1",
|
"ts-morph": "^17.0.1",
|
||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0",
|
||||||
|
"uuid": "^11.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^17.0.0",
|
"@commitlint/cli": "^17.0.0",
|
||||||
@ -3014,6 +3015,15 @@
|
|||||||
"node": ">= 0.12"
|
"node": ">= 0.12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@cypress/request/node_modules/uuid": {
|
||||||
|
"version": "8.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||||
|
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||||
|
"dev": true,
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@cypress/xvfb": {
|
"node_modules/@cypress/xvfb": {
|
||||||
"version": "1.2.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz",
|
||||||
@ -30831,6 +30841,15 @@
|
|||||||
"websocket-driver": "^0.7.4"
|
"websocket-driver": "^0.7.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sockjs/node_modules/uuid": {
|
||||||
|
"version": "8.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||||
|
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||||
|
"dev": true,
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/source-map": {
|
"node_modules/source-map": {
|
||||||
"version": "0.6.1",
|
"version": "0.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||||
@ -33282,12 +33301,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/uuid": {
|
"node_modules/uuid": {
|
||||||
"version": "8.3.2",
|
"version": "11.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
|
||||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
|
||||||
"dev": true,
|
"funding": [
|
||||||
|
"https://github.com/sponsors/broofa",
|
||||||
|
"https://github.com/sponsors/ctavan"
|
||||||
|
],
|
||||||
"bin": {
|
"bin": {
|
||||||
"uuid": "dist/bin/uuid"
|
"uuid": "dist/esm/bin/uuid"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/uvu": {
|
"node_modules/uvu": {
|
||||||
|
|||||||
@ -62,7 +62,8 @@
|
|||||||
"regenerator-runtime": "0.13.7",
|
"regenerator-runtime": "0.13.7",
|
||||||
"robot3": "^0.4.1",
|
"robot3": "^0.4.1",
|
||||||
"ts-morph": "^17.0.1",
|
"ts-morph": "^17.0.1",
|
||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0",
|
||||||
|
"uuid": "^11.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^17.0.0",
|
"@commitlint/cli": "^17.0.0",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user