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
|
||||
echo "Script executed with args: $1 $2"
|
||||
echo "Current time: $(date)"
|
||||
echo "Current directory: $(pwd)"
|
||||
|
||||
output_folder="/var/www/html/watermarked_videos/"
|
||||
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 {
|
||||
executeScript,
|
||||
getQueueStats,
|
||||
getScriptExecution,
|
||||
} from "../../../services/bashExecService";
|
||||
|
||||
// Modified POST method that returns a token instead of waiting for execution result
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
@ -16,22 +17,33 @@ export async function POST(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
const result = await executeScript({
|
||||
const token = executeScript({
|
||||
scriptPath,
|
||||
args,
|
||||
});
|
||||
|
||||
return NextResponse.json(result);
|
||||
return NextResponse.json({ token });
|
||||
} catch (error: any) {
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// Endpoint to get queue statistics
|
||||
export async function GET() {
|
||||
// API to query execution result by token from path parameter
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const stats = getQueueStats();
|
||||
return NextResponse.json(stats);
|
||||
// Get token from the search params in the URL
|
||||
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) {
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
|
||||
@ -1,8 +1,13 @@
|
||||
import { execa } from "execa";
|
||||
import PQueue from "p-queue";
|
||||
import { v4 as uuidv4 } from "uuid"; // Make sure to install this package
|
||||
|
||||
// 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 = {
|
||||
scriptPath: string;
|
||||
@ -17,37 +22,76 @@ export type ScriptResult = {
|
||||
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(
|
||||
params: ScriptParams,
|
||||
): Promise<ScriptResult> {
|
||||
// Add the script execution to the queue
|
||||
|
||||
// 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",
|
||||
};
|
||||
function cleanupExpiredResults() {
|
||||
const now = Date.now();
|
||||
for (const [token, data] of executionResults.entries()) {
|
||||
if (now - data.timestamp > RESULT_EXPIRY) {
|
||||
executionResults.delete(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
// 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
|
||||
const { stdout, stderr } = await execa(
|
||||
"bash",
|
||||
[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());
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
stdout: stdout ? stdout.toString() : "",
|
||||
stderr: stderr ? stderr.toString() : "",
|
||||
};
|
||||
// Store successful result
|
||||
executionResults.set(token, {
|
||||
status: "completed",
|
||||
result: {
|
||||
success: true,
|
||||
stdout: stdout ? stdout.toString() : "",
|
||||
stderr: stderr ? stderr.toString() : "",
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
} catch (error: any) {
|
||||
// Handle script execution errors
|
||||
return {
|
||||
success: false,
|
||||
stdout: error.stdout ? error.stdout.toString() : "",
|
||||
stderr: error.stderr ? error.stderr.toString() : "",
|
||||
error: error.message || "Failed to execute script",
|
||||
};
|
||||
executionResults.set(token, {
|
||||
status: "failed",
|
||||
result: {
|
||||
success: false,
|
||||
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
|
||||
@ -81,6 +162,7 @@ export function getQueueStats() {
|
||||
size: scriptQueue.size,
|
||||
pending: scriptQueue.pending,
|
||||
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",
|
||||
"robot3": "^0.4.1",
|
||||
"ts-morph": "^17.0.1",
|
||||
"tslib": "^2.3.0"
|
||||
"tslib": "^2.3.0",
|
||||
"uuid": "^11.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^17.0.0",
|
||||
@ -3014,6 +3015,15 @@
|
||||
"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": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz",
|
||||
@ -30831,6 +30841,15 @@
|
||||
"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": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
@ -33282,12 +33301,15 @@
|
||||
}
|
||||
},
|
||||
"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,
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
|
||||
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
"uuid": "dist/esm/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/uvu": {
|
||||
|
||||
@ -62,7 +62,8 @@
|
||||
"regenerator-runtime": "0.13.7",
|
||||
"robot3": "^0.4.1",
|
||||
"ts-morph": "^17.0.1",
|
||||
"tslib": "^2.3.0"
|
||||
"tslib": "^2.3.0",
|
||||
"uuid": "^11.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^17.0.0",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user