From 03002f49719c7655d0af8a7bf729c75e0adee72f Mon Sep 17 00:00:00 2001 From: -LAN- Date: Sat, 25 Oct 2025 18:23:27 +0800 Subject: [PATCH 1/2] Add Swagger docs for file download endpoints (#27374) Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com> --- api/controllers/files/image_preview.py | 54 ++++++++++++++++++++++++-- api/controllers/files/tool_files.py | 20 ++++++++++ 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/api/controllers/files/image_preview.py b/api/controllers/files/image_preview.py index 3db82456d5..d320855f29 100644 --- a/api/controllers/files/image_preview.py +++ b/api/controllers/files/image_preview.py @@ -14,10 +14,25 @@ from services.file_service import FileService @files_ns.route("//image-preview") class ImagePreviewApi(Resource): - """ - Deprecated - """ + """Deprecated endpoint for retrieving image previews.""" + @files_ns.doc("get_image_preview") + @files_ns.doc(description="Retrieve a signed image preview for a file") + @files_ns.doc( + params={ + "file_id": "ID of the file to preview", + "timestamp": "Unix timestamp used in the signature", + "nonce": "Random string used in the signature", + "sign": "HMAC signature verifying the request", + } + ) + @files_ns.doc( + responses={ + 200: "Image preview returned successfully", + 400: "Missing or invalid signature parameters", + 415: "Unsupported file type", + } + ) def get(self, file_id): file_id = str(file_id) @@ -43,6 +58,25 @@ class ImagePreviewApi(Resource): @files_ns.route("//file-preview") class FilePreviewApi(Resource): + @files_ns.doc("get_file_preview") + @files_ns.doc(description="Download a file preview or attachment using signed parameters") + @files_ns.doc( + params={ + "file_id": "ID of the file to preview", + "timestamp": "Unix timestamp used in the signature", + "nonce": "Random string used in the signature", + "sign": "HMAC signature verifying the request", + "as_attachment": "Whether to download the file as an attachment", + } + ) + @files_ns.doc( + responses={ + 200: "File stream returned successfully", + 400: "Missing or invalid signature parameters", + 404: "File not found", + 415: "Unsupported file type", + } + ) def get(self, file_id): file_id = str(file_id) @@ -101,6 +135,20 @@ class FilePreviewApi(Resource): @files_ns.route("/workspaces//webapp-logo") class WorkspaceWebappLogoApi(Resource): + @files_ns.doc("get_workspace_webapp_logo") + @files_ns.doc(description="Fetch the custom webapp logo for a workspace") + @files_ns.doc( + params={ + "workspace_id": "Workspace identifier", + } + ) + @files_ns.doc( + responses={ + 200: "Logo returned successfully", + 404: "Webapp logo not configured", + 415: "Unsupported file type", + } + ) def get(self, workspace_id): workspace_id = str(workspace_id) diff --git a/api/controllers/files/tool_files.py b/api/controllers/files/tool_files.py index dec5a4a1b2..ecaeb85821 100644 --- a/api/controllers/files/tool_files.py +++ b/api/controllers/files/tool_files.py @@ -13,6 +13,26 @@ from extensions.ext_database import db as global_db @files_ns.route("/tools/.") class ToolFileApi(Resource): + @files_ns.doc("get_tool_file") + @files_ns.doc(description="Download a tool file by ID using signed parameters") + @files_ns.doc( + params={ + "file_id": "Tool file identifier", + "extension": "Expected file extension", + "timestamp": "Unix timestamp used in the signature", + "nonce": "Random string used in the signature", + "sign": "HMAC signature verifying the request", + "as_attachment": "Whether to download the file as an attachment", + } + ) + @files_ns.doc( + responses={ + 200: "Tool file stream returned successfully", + 403: "Forbidden - invalid signature", + 404: "File not found", + 415: "Unsupported file type", + } + ) def get(self, file_id, extension): file_id = str(file_id) From 82be30568014083cfaad93c5785b7bd0bc4fdeb1 Mon Sep 17 00:00:00 2001 From: MelodicGin <4485145+gin-melodic@users.noreply.github.com> Date: Sun, 26 Oct 2025 11:53:56 +0800 Subject: [PATCH 2/2] Bugfix: Windows compatibility issue with 'cp' command not found when running pnpm start. (#25670) (#25672) Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com> --- web/package.json | 2 +- web/scripts/copy-and-start.mjs | 115 +++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 web/scripts/copy-and-start.mjs diff --git a/web/package.json b/web/package.json index abc0914469..47cd1c9374 100644 --- a/web/package.json +++ b/web/package.json @@ -22,7 +22,7 @@ "dev": "cross-env NODE_OPTIONS='--inspect' next dev --turbopack", "build": "next build", "build:docker": "next build && node scripts/optimize-standalone.js", - "start": "cp -r .next/static .next/standalone/.next/static && cp -r public .next/standalone/public && cross-env PORT=$npm_config_port HOSTNAME=$npm_config_host node .next/standalone/server.js", + "start": "node ./scripts/copy-and-start.mjs", "lint": "eslint --cache --cache-location node_modules/.cache/eslint/.eslint-cache", "lint:fix": "eslint --cache --cache-location node_modules/.cache/eslint/.eslint-cache --fix", "lint:quiet": "eslint --cache --cache-location node_modules/.cache/eslint/.eslint-cache --quiet", diff --git a/web/scripts/copy-and-start.mjs b/web/scripts/copy-and-start.mjs new file mode 100644 index 0000000000..b23ce636a4 --- /dev/null +++ b/web/scripts/copy-and-start.mjs @@ -0,0 +1,115 @@ +#!/usr/bin/env node +/** + * This script copies static files to the target directory and starts the server. + * It is intended to be used as a replacement for `next start`. + */ + +import { cp, mkdir, stat } from 'node:fs/promises' +import { spawn } from 'node:child_process' +import path from 'node:path' + +// Configuration for directories to copy +const DIRS_TO_COPY = [ + { + src: path.join('.next', 'static'), + dest: path.join('.next', 'standalone', '.next', 'static'), + }, + { + src: 'public', + dest: path.join('.next', 'standalone', 'public'), + }, +] + +// Path to the server script +const SERVER_SCRIPT_PATH = path.join('.next', 'standalone', 'server.js') + +// Function to check if a path exists +const pathExists = async (path) => { + try { + console.debug(`Checking if path exists: ${path}`) + await stat(path) + console.debug(`Path exists: ${path}`) + return true + } + catch (err) { + if (err.code === 'ENOENT') { + console.warn(`Path does not exist: ${path}`) + return false + } + throw err + } +} + +// Function to recursively copy directories +const copyDir = async (src, dest) => { + console.debug(`Copying directory from ${src} to ${dest}`) + await cp(src, dest, { recursive: true }) + console.info(`Successfully copied ${src} to ${dest}`) +} + +// Process each directory copy operation +const copyAllDirs = async () => { + console.debug('Starting directory copy operations') + for (const { src, dest } of DIRS_TO_COPY) { + try { + // Instead of pre-creating destination directory, we ensure parent directory exists + const destParent = path.dirname(dest) + console.debug(`Ensuring destination parent directory exists: ${destParent}`) + await mkdir(destParent, { recursive: true }) + if (await pathExists(src)) { + await copyDir(src, dest) + } + else { + console.error(`Error: ${src} directory does not exist. This is a required build artifact.`) + process.exit(1) + } + } + catch (err) { + console.error(`Error processing ${src}:`, err.message) + process.exit(1) + } + } + console.debug('Finished directory copy operations') +} + +// Run copy operations and start server +const main = async () => { + console.debug('Starting copy-and-start script') + await copyAllDirs() + + // Start server + const port = process.env.npm_config_port || process.env.PORT || '3000' + const host = process.env.npm_config_host || process.env.HOSTNAME || '0.0.0.0' + + console.info(`Starting server on ${host}:${port}`) + console.debug(`Server script path: ${SERVER_SCRIPT_PATH}`) + console.debug(`Environment variables - PORT: ${port}, HOSTNAME: ${host}`) + + const server = spawn( + process.execPath, + [SERVER_SCRIPT_PATH], + { + env: { + ...process.env, + PORT: port, + HOSTNAME: host, + }, + stdio: 'inherit', + }, + ) + + server.on('error', (err) => { + console.error('Failed to start server:', err) + process.exit(1) + }) + + server.on('exit', (code) => { + console.debug(`Server exited with code: ${code}`) + process.exit(code || 0) + }) +} + +main().catch((err) => { + console.error('Unexpected error:', err) + process.exit(1) +})