Spaces:
Running
Running
Add form data and file upload support with UI controls
Browse files- Add form endpoints: /form/login, /form/contact for URL-encoded data
- Add file upload endpoints: /upload/file, /upload/files, /upload/file-with-data
- Add file management: GET /upload/files, GET/DELETE /upload/file/{id}
- Add Form tab with dynamic field management in UI
- Add File tab with drag-and-drop upload area
- Add code snippet panel to display generated cURL/Python/fetch/HTTPie code
- Update examples grid with form and file upload examples
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
app.py
CHANGED
|
@@ -10,12 +10,16 @@ Endpoints cover:
|
|
| 10 |
- Headers, Status Codes, and more
|
| 11 |
"""
|
| 12 |
|
| 13 |
-
from fastapi import FastAPI, Query, Path, Body, Header, Response, HTTPException, Request
|
| 14 |
from fastapi.responses import PlainTextResponse, HTMLResponse, JSONResponse
|
| 15 |
from pydantic import BaseModel, Field
|
| 16 |
from typing import Optional, List
|
| 17 |
from datetime import datetime
|
| 18 |
import json
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
app = FastAPI(
|
| 21 |
title="API Teaching Tool",
|
|
@@ -69,6 +73,7 @@ def api_info():
|
|
| 69 |
"PUT examples": ["/items/{id}"],
|
| 70 |
"DELETE examples": ["/items/{id}"],
|
| 71 |
"PATCH examples": ["/items/{id}"],
|
|
|
|
| 72 |
"Response formats": ["/format/json", "/format/text", "/format/html", "/format/xml"],
|
| 73 |
"Educational": ["/echo", "/headers", "/status/{code}"],
|
| 74 |
}
|
|
@@ -381,6 +386,130 @@ def ui():
|
|
| 381 |
.json-boolean { color: #aa0d91; }
|
| 382 |
.json-null { color: #86868b; }
|
| 383 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 384 |
/* Preview */
|
| 385 |
.preview-container {
|
| 386 |
padding: 16px;
|
|
@@ -481,12 +610,56 @@ def ui():
|
|
| 481 |
<div class="panel">
|
| 482 |
<div class="tabs" id="request-tabs">
|
| 483 |
<button class="tab active" data-tab="req-body">Body</button>
|
|
|
|
|
|
|
| 484 |
<button class="tab" data-tab="req-headers">Headers</button>
|
| 485 |
</div>
|
| 486 |
<div class="panel-body">
|
| 487 |
<div class="tab-content active" id="req-body-tab">
|
| 488 |
<textarea id="body" placeholder='{"name": "Laptop", "price": 999.99, "quantity": 5}'></textarea>
|
| 489 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 490 |
<div class="tab-content" id="req-headers-tab">
|
| 491 |
<textarea id="headers" placeholder="Content-Type: application/json">Content-Type: application/json</textarea>
|
| 492 |
</div>
|
|
@@ -530,6 +703,18 @@ def ui():
|
|
| 530 |
<h3>Examples</h3>
|
| 531 |
<div class="examples-grid" id="examples"></div>
|
| 532 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 533 |
</div>
|
| 534 |
|
| 535 |
<script>
|
|
@@ -545,18 +730,21 @@ def ui():
|
|
| 545 |
{ method: 'PUT', url: '/items/1', title: 'Replace Item', body: '{"name": "Green Apple", "price": 2.50, "quantity": 200}' },
|
| 546 |
{ method: 'PATCH', url: '/items/1', title: 'Partial Update', body: '{"price": 1.99}' },
|
| 547 |
{ method: 'DELETE', url: '/items/3', title: 'Delete Item' },
|
|
|
|
|
|
|
|
|
|
|
|
|
| 548 |
{ method: 'GET', url: '/format/json', title: 'JSON Format' },
|
| 549 |
{ method: 'GET', url: '/format/xml', title: 'XML Format' },
|
| 550 |
{ method: 'GET', url: '/format/html', title: 'HTML Format' },
|
| 551 |
{ method: 'GET', url: '/format/csv', title: 'CSV Format' },
|
| 552 |
-
{ method: 'GET', url: '/format/yaml', title: 'YAML Format' },
|
| 553 |
-
{ method: 'GET', url: '/format/markdown', title: 'Markdown' },
|
| 554 |
-
{ method: 'GET', url: '/format/image', title: 'PNG Image' },
|
| 555 |
{ method: 'GET', url: '/headers', title: 'View Headers' },
|
| 556 |
{ method: 'GET', url: '/status/404', title: 'Error 404' },
|
| 557 |
-
{ method: 'GET', url: '/status/201', title: 'Status 201' },
|
| 558 |
];
|
| 559 |
|
|
|
|
|
|
|
|
|
|
| 560 |
// Render examples
|
| 561 |
const examplesContainer = document.getElementById('examples');
|
| 562 |
examples.forEach(ex => {
|
|
@@ -573,10 +761,52 @@ def ui():
|
|
| 573 |
document.getElementById('method').value = ex.method;
|
| 574 |
document.getElementById('url').value = ex.url;
|
| 575 |
document.getElementById('body').value = ex.body || '';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 576 |
};
|
| 577 |
examplesContainer.appendChild(btn);
|
| 578 |
});
|
| 579 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 580 |
// Tab switching
|
| 581 |
function setupTabs(containerId) {
|
| 582 |
const container = document.getElementById(containerId);
|
|
@@ -604,12 +834,35 @@ def ui():
|
|
| 604 |
.replace(/: (null)/g, ': <span class="json-null">$1</span>');
|
| 605 |
}
|
| 606 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 607 |
// Send request
|
| 608 |
async function sendRequest() {
|
| 609 |
const method = document.getElementById('method').value;
|
| 610 |
const url = document.getElementById('url').value;
|
| 611 |
const body = document.getElementById('body').value;
|
| 612 |
const headersText = document.getElementById('headers').value;
|
|
|
|
| 613 |
|
| 614 |
const headers = {};
|
| 615 |
headersText.split('\\n').forEach(line => {
|
|
@@ -618,8 +871,32 @@ def ui():
|
|
| 618 |
});
|
| 619 |
|
| 620 |
const opts = { method, headers };
|
| 621 |
-
|
| 622 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 623 |
}
|
| 624 |
|
| 625 |
document.getElementById('send').disabled = true;
|
|
@@ -928,8 +1205,13 @@ def ui():
|
|
| 928 |
e.stopPropagation();
|
| 929 |
const format = btn.dataset.format;
|
| 930 |
const code = generateCode(format);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 931 |
try {
|
| 932 |
-
await navigator.clipboard.writeText(
|
| 933 |
showToast(`Copied as ${format.toUpperCase()}!`);
|
| 934 |
} catch {
|
| 935 |
showToast('Failed to copy');
|
|
@@ -937,6 +1219,114 @@ def ui():
|
|
| 937 |
copyMenu.classList.remove('show');
|
| 938 |
};
|
| 939 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 940 |
</script>
|
| 941 |
</body>
|
| 942 |
</html>
|
|
@@ -1086,6 +1476,209 @@ async def echo_text(request: Request):
|
|
| 1086 |
"content_type": request.headers.get("content-type", "not specified")
|
| 1087 |
}
|
| 1088 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1089 |
# ============================================================================
|
| 1090 |
# DIFFERENT RESPONSE FORMATS
|
| 1091 |
# ============================================================================
|
|
|
|
| 10 |
- Headers, Status Codes, and more
|
| 11 |
"""
|
| 12 |
|
| 13 |
+
from fastapi import FastAPI, Query, Path, Body, Header, Response, HTTPException, Request, File, UploadFile, Form
|
| 14 |
from fastapi.responses import PlainTextResponse, HTMLResponse, JSONResponse
|
| 15 |
from pydantic import BaseModel, Field
|
| 16 |
from typing import Optional, List
|
| 17 |
from datetime import datetime
|
| 18 |
import json
|
| 19 |
+
import base64
|
| 20 |
+
|
| 21 |
+
# In-memory storage for uploaded files
|
| 22 |
+
uploaded_files_db = {}
|
| 23 |
|
| 24 |
app = FastAPI(
|
| 25 |
title="API Teaching Tool",
|
|
|
|
| 73 |
"PUT examples": ["/items/{id}"],
|
| 74 |
"DELETE examples": ["/items/{id}"],
|
| 75 |
"PATCH examples": ["/items/{id}"],
|
| 76 |
+
"Form & File Upload": ["/form/login", "/form/contact", "/upload/file", "/upload/files", "/upload/file-with-data"],
|
| 77 |
"Response formats": ["/format/json", "/format/text", "/format/html", "/format/xml"],
|
| 78 |
"Educational": ["/echo", "/headers", "/status/{code}"],
|
| 79 |
}
|
|
|
|
| 386 |
.json-boolean { color: #aa0d91; }
|
| 387 |
.json-null { color: #86868b; }
|
| 388 |
|
| 389 |
+
/* Form fields */
|
| 390 |
+
.form-group { margin-bottom: 12px; }
|
| 391 |
+
.form-label {
|
| 392 |
+
display: block;
|
| 393 |
+
font-size: 12px;
|
| 394 |
+
font-weight: 500;
|
| 395 |
+
color: #86868b;
|
| 396 |
+
margin-bottom: 4px;
|
| 397 |
+
}
|
| 398 |
+
.form-input {
|
| 399 |
+
width: 100%;
|
| 400 |
+
padding: 8px 12px;
|
| 401 |
+
background: #f5f5f7;
|
| 402 |
+
border: 1px solid transparent;
|
| 403 |
+
border-radius: 6px;
|
| 404 |
+
font-size: 14px;
|
| 405 |
+
transition: border-color 0.2s;
|
| 406 |
+
}
|
| 407 |
+
.form-input:focus { border-color: #007aff; outline: none; }
|
| 408 |
+
.form-row { display: flex; gap: 12px; }
|
| 409 |
+
.form-row .form-group { flex: 1; }
|
| 410 |
+
.checkbox-group {
|
| 411 |
+
display: flex;
|
| 412 |
+
align-items: center;
|
| 413 |
+
gap: 8px;
|
| 414 |
+
}
|
| 415 |
+
.checkbox-group input { width: 16px; height: 16px; }
|
| 416 |
+
|
| 417 |
+
/* File upload */
|
| 418 |
+
.file-upload-area {
|
| 419 |
+
border: 2px dashed #d2d2d7;
|
| 420 |
+
border-radius: 8px;
|
| 421 |
+
padding: 24px;
|
| 422 |
+
text-align: center;
|
| 423 |
+
transition: all 0.2s;
|
| 424 |
+
cursor: pointer;
|
| 425 |
+
background: #fafafa;
|
| 426 |
+
}
|
| 427 |
+
.file-upload-area:hover { border-color: #007aff; background: #f0f7ff; }
|
| 428 |
+
.file-upload-area.dragover { border-color: #007aff; background: #e8f4ff; }
|
| 429 |
+
.file-upload-icon { font-size: 32px; margin-bottom: 8px; }
|
| 430 |
+
.file-upload-text { font-size: 13px; color: #86868b; }
|
| 431 |
+
.file-upload-text strong { color: #007aff; }
|
| 432 |
+
.file-input { display: none; }
|
| 433 |
+
.file-list { margin-top: 12px; }
|
| 434 |
+
.file-item {
|
| 435 |
+
display: flex;
|
| 436 |
+
align-items: center;
|
| 437 |
+
justify-content: space-between;
|
| 438 |
+
padding: 8px 12px;
|
| 439 |
+
background: #f5f5f7;
|
| 440 |
+
border-radius: 6px;
|
| 441 |
+
margin-bottom: 6px;
|
| 442 |
+
font-size: 13px;
|
| 443 |
+
}
|
| 444 |
+
.file-item-name { font-weight: 500; }
|
| 445 |
+
.file-item-size { color: #86868b; font-size: 11px; }
|
| 446 |
+
.file-item-remove {
|
| 447 |
+
background: none;
|
| 448 |
+
border: none;
|
| 449 |
+
color: #ff3b30;
|
| 450 |
+
cursor: pointer;
|
| 451 |
+
font-size: 16px;
|
| 452 |
+
padding: 0 4px;
|
| 453 |
+
}
|
| 454 |
+
|
| 455 |
+
/* Code snippet panel */
|
| 456 |
+
.code-panel {
|
| 457 |
+
margin-top: 20px;
|
| 458 |
+
background: #fff;
|
| 459 |
+
border-radius: 10px;
|
| 460 |
+
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
|
| 461 |
+
overflow: hidden;
|
| 462 |
+
display: none;
|
| 463 |
+
}
|
| 464 |
+
.code-panel.show { display: block; }
|
| 465 |
+
.code-panel-header {
|
| 466 |
+
display: flex;
|
| 467 |
+
align-items: center;
|
| 468 |
+
justify-content: space-between;
|
| 469 |
+
padding: 12px 16px;
|
| 470 |
+
background: #fafafa;
|
| 471 |
+
border-bottom: 1px solid #d2d2d7;
|
| 472 |
+
}
|
| 473 |
+
.code-panel-title { font-weight: 600; font-size: 13px; }
|
| 474 |
+
.code-panel-close {
|
| 475 |
+
background: none;
|
| 476 |
+
border: none;
|
| 477 |
+
font-size: 18px;
|
| 478 |
+
color: #86868b;
|
| 479 |
+
cursor: pointer;
|
| 480 |
+
padding: 0 4px;
|
| 481 |
+
}
|
| 482 |
+
.code-panel-close:hover { color: #1d1d1f; }
|
| 483 |
+
.code-panel-content {
|
| 484 |
+
padding: 16px;
|
| 485 |
+
font-family: 'SF Mono', Menlo, monospace;
|
| 486 |
+
font-size: 13px;
|
| 487 |
+
line-height: 1.6;
|
| 488 |
+
white-space: pre-wrap;
|
| 489 |
+
word-break: break-all;
|
| 490 |
+
background: #1d1d1f;
|
| 491 |
+
color: #f5f5f7;
|
| 492 |
+
max-height: 300px;
|
| 493 |
+
overflow: auto;
|
| 494 |
+
}
|
| 495 |
+
.code-panel-actions {
|
| 496 |
+
padding: 12px 16px;
|
| 497 |
+
background: #fafafa;
|
| 498 |
+
border-top: 1px solid #d2d2d7;
|
| 499 |
+
display: flex;
|
| 500 |
+
gap: 8px;
|
| 501 |
+
}
|
| 502 |
+
.code-copy-btn {
|
| 503 |
+
padding: 6px 12px;
|
| 504 |
+
background: #007aff;
|
| 505 |
+
color: #fff;
|
| 506 |
+
border: none;
|
| 507 |
+
border-radius: 6px;
|
| 508 |
+
font-size: 12px;
|
| 509 |
+
cursor: pointer;
|
| 510 |
+
}
|
| 511 |
+
.code-copy-btn:hover { background: #0056b3; }
|
| 512 |
+
|
| 513 |
/* Preview */
|
| 514 |
.preview-container {
|
| 515 |
padding: 16px;
|
|
|
|
| 610 |
<div class="panel">
|
| 611 |
<div class="tabs" id="request-tabs">
|
| 612 |
<button class="tab active" data-tab="req-body">Body</button>
|
| 613 |
+
<button class="tab" data-tab="req-form">Form</button>
|
| 614 |
+
<button class="tab" data-tab="req-file">File</button>
|
| 615 |
<button class="tab" data-tab="req-headers">Headers</button>
|
| 616 |
</div>
|
| 617 |
<div class="panel-body">
|
| 618 |
<div class="tab-content active" id="req-body-tab">
|
| 619 |
<textarea id="body" placeholder='{"name": "Laptop", "price": 999.99, "quantity": 5}'></textarea>
|
| 620 |
</div>
|
| 621 |
+
<div class="tab-content" id="req-form-tab">
|
| 622 |
+
<div id="form-fields">
|
| 623 |
+
<div class="form-row">
|
| 624 |
+
<div class="form-group">
|
| 625 |
+
<label class="form-label">Field Name</label>
|
| 626 |
+
<input type="text" class="form-input form-field-name" placeholder="username">
|
| 627 |
+
</div>
|
| 628 |
+
<div class="form-group">
|
| 629 |
+
<label class="form-label">Value</label>
|
| 630 |
+
<input type="text" class="form-input form-field-value" placeholder="john_doe">
|
| 631 |
+
</div>
|
| 632 |
+
</div>
|
| 633 |
+
</div>
|
| 634 |
+
<button type="button" id="add-form-field" style="margin-top:8px;padding:6px 12px;background:#f5f5f7;border:1px solid #d2d2d7;border-radius:6px;font-size:12px;cursor:pointer;">+ Add Field</button>
|
| 635 |
+
<div style="margin-top:12px;font-size:11px;color:#86868b;">
|
| 636 |
+
Content-Type: application/x-www-form-urlencoded
|
| 637 |
+
</div>
|
| 638 |
+
</div>
|
| 639 |
+
<div class="tab-content" id="req-file-tab">
|
| 640 |
+
<div class="file-upload-area" id="file-drop-area">
|
| 641 |
+
<div class="file-upload-icon">📁</div>
|
| 642 |
+
<div class="file-upload-text">
|
| 643 |
+
<strong>Click to upload</strong> or drag and drop<br>
|
| 644 |
+
<span style="font-size:11px;">Any file type supported</span>
|
| 645 |
+
</div>
|
| 646 |
+
<input type="file" id="file-input" class="file-input" multiple>
|
| 647 |
+
</div>
|
| 648 |
+
<div class="file-list" id="file-list"></div>
|
| 649 |
+
<div id="file-metadata" style="margin-top:12px;display:none;">
|
| 650 |
+
<div class="form-group">
|
| 651 |
+
<label class="form-label">Title (optional)</label>
|
| 652 |
+
<input type="text" class="form-input" id="file-title" placeholder="My Document">
|
| 653 |
+
</div>
|
| 654 |
+
<div class="form-group">
|
| 655 |
+
<label class="form-label">Description (optional)</label>
|
| 656 |
+
<input type="text" class="form-input" id="file-description" placeholder="A description of the file">
|
| 657 |
+
</div>
|
| 658 |
+
</div>
|
| 659 |
+
<div style="margin-top:12px;font-size:11px;color:#86868b;">
|
| 660 |
+
Content-Type: multipart/form-data
|
| 661 |
+
</div>
|
| 662 |
+
</div>
|
| 663 |
<div class="tab-content" id="req-headers-tab">
|
| 664 |
<textarea id="headers" placeholder="Content-Type: application/json">Content-Type: application/json</textarea>
|
| 665 |
</div>
|
|
|
|
| 703 |
<h3>Examples</h3>
|
| 704 |
<div class="examples-grid" id="examples"></div>
|
| 705 |
</div>
|
| 706 |
+
|
| 707 |
+
<!-- Code Snippet Panel -->
|
| 708 |
+
<div class="code-panel" id="code-panel">
|
| 709 |
+
<div class="code-panel-header">
|
| 710 |
+
<span class="code-panel-title" id="code-panel-title">cURL</span>
|
| 711 |
+
<button class="code-panel-close" id="code-panel-close">×</button>
|
| 712 |
+
</div>
|
| 713 |
+
<div class="code-panel-content" id="code-panel-content"></div>
|
| 714 |
+
<div class="code-panel-actions">
|
| 715 |
+
<button class="code-copy-btn" id="code-copy-btn">Copy to Clipboard</button>
|
| 716 |
+
</div>
|
| 717 |
+
</div>
|
| 718 |
</div>
|
| 719 |
|
| 720 |
<script>
|
|
|
|
| 730 |
{ method: 'PUT', url: '/items/1', title: 'Replace Item', body: '{"name": "Green Apple", "price": 2.50, "quantity": 200}' },
|
| 731 |
{ method: 'PATCH', url: '/items/1', title: 'Partial Update', body: '{"price": 1.99}' },
|
| 732 |
{ method: 'DELETE', url: '/items/3', title: 'Delete Item' },
|
| 733 |
+
{ method: 'POST', url: '/form/login', title: 'Form Login', formData: {username: 'john_doe', password: 'secret123', remember_me: 'true'} },
|
| 734 |
+
{ method: 'POST', url: '/form/contact', title: 'Contact Form', formData: {name: 'Alice', email: 'alice@example.com', subject: 'Hello', message: 'Nice API!'} },
|
| 735 |
+
{ method: 'POST', url: '/upload/file', title: 'File Upload', isFileUpload: true },
|
| 736 |
+
{ method: 'GET', url: '/upload/files', title: 'List Uploads' },
|
| 737 |
{ method: 'GET', url: '/format/json', title: 'JSON Format' },
|
| 738 |
{ method: 'GET', url: '/format/xml', title: 'XML Format' },
|
| 739 |
{ method: 'GET', url: '/format/html', title: 'HTML Format' },
|
| 740 |
{ method: 'GET', url: '/format/csv', title: 'CSV Format' },
|
|
|
|
|
|
|
|
|
|
| 741 |
{ method: 'GET', url: '/headers', title: 'View Headers' },
|
| 742 |
{ method: 'GET', url: '/status/404', title: 'Error 404' },
|
|
|
|
| 743 |
];
|
| 744 |
|
| 745 |
+
// Track selected files
|
| 746 |
+
let selectedFiles = [];
|
| 747 |
+
|
| 748 |
// Render examples
|
| 749 |
const examplesContainer = document.getElementById('examples');
|
| 750 |
examples.forEach(ex => {
|
|
|
|
| 761 |
document.getElementById('method').value = ex.method;
|
| 762 |
document.getElementById('url').value = ex.url;
|
| 763 |
document.getElementById('body').value = ex.body || '';
|
| 764 |
+
|
| 765 |
+
// Handle form data examples
|
| 766 |
+
if (ex.formData) {
|
| 767 |
+
switchToTab('request-tabs', 'req-form');
|
| 768 |
+
populateFormFields(ex.formData);
|
| 769 |
+
} else if (ex.isFileUpload) {
|
| 770 |
+
switchToTab('request-tabs', 'req-file');
|
| 771 |
+
} else {
|
| 772 |
+
switchToTab('request-tabs', 'req-body');
|
| 773 |
+
}
|
| 774 |
};
|
| 775 |
examplesContainer.appendChild(btn);
|
| 776 |
});
|
| 777 |
|
| 778 |
+
function switchToTab(tabsId, tabName) {
|
| 779 |
+
const container = document.getElementById(tabsId);
|
| 780 |
+
container.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
| 781 |
+
const tab = container.querySelector(`[data-tab="${tabName}"]`);
|
| 782 |
+
if (tab) {
|
| 783 |
+
tab.classList.add('active');
|
| 784 |
+
const panel = container.closest('.panel') || document;
|
| 785 |
+
panel.querySelectorAll(':scope > .tab-content, :scope > .panel-body > .tab-content').forEach(c => c.classList.remove('active'));
|
| 786 |
+
document.getElementById(tabName + '-tab').classList.add('active');
|
| 787 |
+
}
|
| 788 |
+
}
|
| 789 |
+
|
| 790 |
+
function populateFormFields(data) {
|
| 791 |
+
const container = document.getElementById('form-fields');
|
| 792 |
+
container.innerHTML = '';
|
| 793 |
+
Object.entries(data).forEach(([key, value], i) => {
|
| 794 |
+
const row = document.createElement('div');
|
| 795 |
+
row.className = 'form-row';
|
| 796 |
+
row.innerHTML = `
|
| 797 |
+
<div class="form-group">
|
| 798 |
+
<label class="form-label">${i === 0 ? 'Field Name' : ''}</label>
|
| 799 |
+
<input type="text" class="form-input form-field-name" value="${key}" placeholder="field_name">
|
| 800 |
+
</div>
|
| 801 |
+
<div class="form-group">
|
| 802 |
+
<label class="form-label">${i === 0 ? 'Value' : ''}</label>
|
| 803 |
+
<input type="text" class="form-input form-field-value" value="${value}" placeholder="value">
|
| 804 |
+
</div>
|
| 805 |
+
`;
|
| 806 |
+
container.appendChild(row);
|
| 807 |
+
});
|
| 808 |
+
}
|
| 809 |
+
|
| 810 |
// Tab switching
|
| 811 |
function setupTabs(containerId) {
|
| 812 |
const container = document.getElementById(containerId);
|
|
|
|
| 834 |
.replace(/: (null)/g, ': <span class="json-null">$1</span>');
|
| 835 |
}
|
| 836 |
|
| 837 |
+
// Get active request type
|
| 838 |
+
function getActiveRequestType() {
|
| 839 |
+
const formTab = document.getElementById('req-form-tab');
|
| 840 |
+
const fileTab = document.getElementById('req-file-tab');
|
| 841 |
+
if (formTab.classList.contains('active')) return 'form';
|
| 842 |
+
if (fileTab.classList.contains('active')) return 'file';
|
| 843 |
+
return 'body';
|
| 844 |
+
}
|
| 845 |
+
|
| 846 |
+
// Get form data from the form fields
|
| 847 |
+
function getFormFieldsData() {
|
| 848 |
+
const data = {};
|
| 849 |
+
const names = document.querySelectorAll('.form-field-name');
|
| 850 |
+
const values = document.querySelectorAll('.form-field-value');
|
| 851 |
+
names.forEach((nameInput, i) => {
|
| 852 |
+
const name = nameInput.value.trim();
|
| 853 |
+
const value = values[i]?.value || '';
|
| 854 |
+
if (name) data[name] = value;
|
| 855 |
+
});
|
| 856 |
+
return data;
|
| 857 |
+
}
|
| 858 |
+
|
| 859 |
// Send request
|
| 860 |
async function sendRequest() {
|
| 861 |
const method = document.getElementById('method').value;
|
| 862 |
const url = document.getElementById('url').value;
|
| 863 |
const body = document.getElementById('body').value;
|
| 864 |
const headersText = document.getElementById('headers').value;
|
| 865 |
+
const requestType = getActiveRequestType();
|
| 866 |
|
| 867 |
const headers = {};
|
| 868 |
headersText.split('\\n').forEach(line => {
|
|
|
|
| 871 |
});
|
| 872 |
|
| 873 |
const opts = { method, headers };
|
| 874 |
+
|
| 875 |
+
if (['POST', 'PUT', 'PATCH'].includes(method)) {
|
| 876 |
+
if (requestType === 'form') {
|
| 877 |
+
// Form URL-encoded data
|
| 878 |
+
const formData = getFormFieldsData();
|
| 879 |
+
opts.body = new URLSearchParams(formData).toString();
|
| 880 |
+
opts.headers['Content-Type'] = 'application/x-www-form-urlencoded';
|
| 881 |
+
} else if (requestType === 'file' && selectedFiles.length > 0) {
|
| 882 |
+
// Multipart form data for file upload
|
| 883 |
+
const formData = new FormData();
|
| 884 |
+
if (selectedFiles.length === 1) {
|
| 885 |
+
formData.append('file', selectedFiles[0]);
|
| 886 |
+
} else {
|
| 887 |
+
selectedFiles.forEach(f => formData.append('files', f));
|
| 888 |
+
}
|
| 889 |
+
// Add optional metadata
|
| 890 |
+
const title = document.getElementById('file-title')?.value;
|
| 891 |
+
const description = document.getElementById('file-description')?.value;
|
| 892 |
+
if (title) formData.append('title', title);
|
| 893 |
+
if (description) formData.append('description', description);
|
| 894 |
+
opts.body = formData;
|
| 895 |
+
// Don't set Content-Type for FormData - browser sets it with boundary
|
| 896 |
+
delete opts.headers['Content-Type'];
|
| 897 |
+
} else if (body) {
|
| 898 |
+
opts.body = body;
|
| 899 |
+
}
|
| 900 |
}
|
| 901 |
|
| 902 |
document.getElementById('send').disabled = true;
|
|
|
|
| 1205 |
e.stopPropagation();
|
| 1206 |
const format = btn.dataset.format;
|
| 1207 |
const code = generateCode(format);
|
| 1208 |
+
const formattedCode = code.replace(/\\\\n/g, '\\n');
|
| 1209 |
+
|
| 1210 |
+
// Show in code panel
|
| 1211 |
+
showCodePanel(format, formattedCode);
|
| 1212 |
+
|
| 1213 |
try {
|
| 1214 |
+
await navigator.clipboard.writeText(formattedCode);
|
| 1215 |
showToast(`Copied as ${format.toUpperCase()}!`);
|
| 1216 |
} catch {
|
| 1217 |
showToast('Failed to copy');
|
|
|
|
| 1219 |
copyMenu.classList.remove('show');
|
| 1220 |
};
|
| 1221 |
});
|
| 1222 |
+
|
| 1223 |
+
// Code panel functionality
|
| 1224 |
+
const codePanel = document.getElementById('code-panel');
|
| 1225 |
+
const codePanelTitle = document.getElementById('code-panel-title');
|
| 1226 |
+
const codePanelContent = document.getElementById('code-panel-content');
|
| 1227 |
+
const codePanelClose = document.getElementById('code-panel-close');
|
| 1228 |
+
const codeCopyBtn = document.getElementById('code-copy-btn');
|
| 1229 |
+
|
| 1230 |
+
function showCodePanel(format, code) {
|
| 1231 |
+
const titles = {
|
| 1232 |
+
curl: 'cURL Command',
|
| 1233 |
+
python: 'Python (requests)',
|
| 1234 |
+
fetch: 'JavaScript (fetch)',
|
| 1235 |
+
httpie: 'HTTPie Command'
|
| 1236 |
+
};
|
| 1237 |
+
codePanelTitle.textContent = titles[format] || format.toUpperCase();
|
| 1238 |
+
codePanelContent.textContent = code;
|
| 1239 |
+
codePanel.classList.add('show');
|
| 1240 |
+
}
|
| 1241 |
+
|
| 1242 |
+
codePanelClose.onclick = () => codePanel.classList.remove('show');
|
| 1243 |
+
codeCopyBtn.onclick = async () => {
|
| 1244 |
+
try {
|
| 1245 |
+
await navigator.clipboard.writeText(codePanelContent.textContent);
|
| 1246 |
+
showToast('Copied to clipboard!');
|
| 1247 |
+
} catch {
|
| 1248 |
+
showToast('Failed to copy');
|
| 1249 |
+
}
|
| 1250 |
+
};
|
| 1251 |
+
|
| 1252 |
+
// File upload handling
|
| 1253 |
+
const fileDropArea = document.getElementById('file-drop-area');
|
| 1254 |
+
const fileInput = document.getElementById('file-input');
|
| 1255 |
+
const fileList = document.getElementById('file-list');
|
| 1256 |
+
const fileMetadata = document.getElementById('file-metadata');
|
| 1257 |
+
|
| 1258 |
+
fileDropArea.onclick = () => fileInput.click();
|
| 1259 |
+
|
| 1260 |
+
fileDropArea.ondragover = (e) => {
|
| 1261 |
+
e.preventDefault();
|
| 1262 |
+
fileDropArea.classList.add('dragover');
|
| 1263 |
+
};
|
| 1264 |
+
|
| 1265 |
+
fileDropArea.ondragleave = () => {
|
| 1266 |
+
fileDropArea.classList.remove('dragover');
|
| 1267 |
+
};
|
| 1268 |
+
|
| 1269 |
+
fileDropArea.ondrop = (e) => {
|
| 1270 |
+
e.preventDefault();
|
| 1271 |
+
fileDropArea.classList.remove('dragover');
|
| 1272 |
+
handleFiles(e.dataTransfer.files);
|
| 1273 |
+
};
|
| 1274 |
+
|
| 1275 |
+
fileInput.onchange = (e) => {
|
| 1276 |
+
handleFiles(e.target.files);
|
| 1277 |
+
};
|
| 1278 |
+
|
| 1279 |
+
function handleFiles(files) {
|
| 1280 |
+
selectedFiles = Array.from(files);
|
| 1281 |
+
renderFileList();
|
| 1282 |
+
}
|
| 1283 |
+
|
| 1284 |
+
function renderFileList() {
|
| 1285 |
+
if (selectedFiles.length === 0) {
|
| 1286 |
+
fileList.innerHTML = '';
|
| 1287 |
+
fileMetadata.style.display = 'none';
|
| 1288 |
+
return;
|
| 1289 |
+
}
|
| 1290 |
+
|
| 1291 |
+
fileList.innerHTML = selectedFiles.map((file, i) => `
|
| 1292 |
+
<div class="file-item">
|
| 1293 |
+
<div>
|
| 1294 |
+
<span class="file-item-name">${file.name}</span>
|
| 1295 |
+
<span class="file-item-size">${formatFileSize(file.size)}</span>
|
| 1296 |
+
</div>
|
| 1297 |
+
<button class="file-item-remove" onclick="removeFile(${i})">×</button>
|
| 1298 |
+
</div>
|
| 1299 |
+
`).join('');
|
| 1300 |
+
|
| 1301 |
+
fileMetadata.style.display = 'block';
|
| 1302 |
+
}
|
| 1303 |
+
|
| 1304 |
+
function formatFileSize(bytes) {
|
| 1305 |
+
if (bytes < 1024) return bytes + ' B';
|
| 1306 |
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
| 1307 |
+
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
| 1308 |
+
}
|
| 1309 |
+
|
| 1310 |
+
window.removeFile = function(index) {
|
| 1311 |
+
selectedFiles.splice(index, 1);
|
| 1312 |
+
renderFileList();
|
| 1313 |
+
};
|
| 1314 |
+
|
| 1315 |
+
// Add form field button
|
| 1316 |
+
document.getElementById('add-form-field').onclick = () => {
|
| 1317 |
+
const container = document.getElementById('form-fields');
|
| 1318 |
+
const row = document.createElement('div');
|
| 1319 |
+
row.className = 'form-row';
|
| 1320 |
+
row.innerHTML = `
|
| 1321 |
+
<div class="form-group">
|
| 1322 |
+
<input type="text" class="form-input form-field-name" placeholder="field_name">
|
| 1323 |
+
</div>
|
| 1324 |
+
<div class="form-group">
|
| 1325 |
+
<input type="text" class="form-input form-field-value" placeholder="value">
|
| 1326 |
+
</div>
|
| 1327 |
+
`;
|
| 1328 |
+
container.appendChild(row);
|
| 1329 |
+
};
|
| 1330 |
</script>
|
| 1331 |
</body>
|
| 1332 |
</html>
|
|
|
|
| 1476 |
"content_type": request.headers.get("content-type", "not specified")
|
| 1477 |
}
|
| 1478 |
|
| 1479 |
+
# ============================================================================
|
| 1480 |
+
# FORM & FILE UPLOAD
|
| 1481 |
+
# ============================================================================
|
| 1482 |
+
|
| 1483 |
+
next_file_id = 1
|
| 1484 |
+
|
| 1485 |
+
@app.post("/form/login", tags=["Form & File Upload"])
|
| 1486 |
+
async def form_login(
|
| 1487 |
+
username: str = Form(..., description="Username"),
|
| 1488 |
+
password: str = Form(..., description="Password"),
|
| 1489 |
+
remember_me: bool = Form(default=False, description="Remember me checkbox")
|
| 1490 |
+
):
|
| 1491 |
+
"""POST - Handle form data (like a login form)"""
|
| 1492 |
+
return {
|
| 1493 |
+
"message": "Form data received successfully",
|
| 1494 |
+
"data": {
|
| 1495 |
+
"username": username,
|
| 1496 |
+
"password": "***hidden***",
|
| 1497 |
+
"password_length": len(password),
|
| 1498 |
+
"remember_me": remember_me
|
| 1499 |
+
},
|
| 1500 |
+
"note": "This demonstrates form-urlencoded data (Content-Type: application/x-www-form-urlencoded)"
|
| 1501 |
+
}
|
| 1502 |
+
|
| 1503 |
+
@app.post("/form/contact", tags=["Form & File Upload"])
|
| 1504 |
+
async def form_contact(
|
| 1505 |
+
name: str = Form(..., description="Your name"),
|
| 1506 |
+
email: str = Form(..., description="Your email"),
|
| 1507 |
+
subject: str = Form(default="General Inquiry", description="Subject"),
|
| 1508 |
+
message: str = Form(..., description="Your message")
|
| 1509 |
+
):
|
| 1510 |
+
"""POST - Contact form submission"""
|
| 1511 |
+
return {
|
| 1512 |
+
"message": "Contact form received",
|
| 1513 |
+
"data": {
|
| 1514 |
+
"name": name,
|
| 1515 |
+
"email": email,
|
| 1516 |
+
"subject": subject,
|
| 1517 |
+
"message": message,
|
| 1518 |
+
"message_length": len(message)
|
| 1519 |
+
},
|
| 1520 |
+
"timestamp": datetime.now().isoformat()
|
| 1521 |
+
}
|
| 1522 |
+
|
| 1523 |
+
@app.post("/upload/file", tags=["Form & File Upload"])
|
| 1524 |
+
async def upload_single_file(
|
| 1525 |
+
file: UploadFile = File(..., description="File to upload")
|
| 1526 |
+
):
|
| 1527 |
+
"""POST - Upload a single file"""
|
| 1528 |
+
global next_file_id
|
| 1529 |
+
|
| 1530 |
+
contents = await file.read()
|
| 1531 |
+
file_size = len(contents)
|
| 1532 |
+
|
| 1533 |
+
# Store file metadata and content
|
| 1534 |
+
file_id = next_file_id
|
| 1535 |
+
uploaded_files_db[file_id] = {
|
| 1536 |
+
"id": file_id,
|
| 1537 |
+
"filename": file.filename,
|
| 1538 |
+
"content_type": file.content_type,
|
| 1539 |
+
"size": file_size,
|
| 1540 |
+
"content": base64.b64encode(contents).decode("utf-8")
|
| 1541 |
+
}
|
| 1542 |
+
next_file_id += 1
|
| 1543 |
+
|
| 1544 |
+
return {
|
| 1545 |
+
"message": "File uploaded successfully",
|
| 1546 |
+
"file": {
|
| 1547 |
+
"id": file_id,
|
| 1548 |
+
"filename": file.filename,
|
| 1549 |
+
"content_type": file.content_type,
|
| 1550 |
+
"size": file_size,
|
| 1551 |
+
"size_formatted": f"{file_size / 1024:.2f} KB" if file_size > 1024 else f"{file_size} bytes"
|
| 1552 |
+
},
|
| 1553 |
+
"download_url": f"/upload/file/{file_id}"
|
| 1554 |
+
}
|
| 1555 |
+
|
| 1556 |
+
@app.post("/upload/files", tags=["Form & File Upload"])
|
| 1557 |
+
async def upload_multiple_files(
|
| 1558 |
+
files: List[UploadFile] = File(..., description="Multiple files to upload")
|
| 1559 |
+
):
|
| 1560 |
+
"""POST - Upload multiple files at once"""
|
| 1561 |
+
global next_file_id
|
| 1562 |
+
|
| 1563 |
+
uploaded = []
|
| 1564 |
+
for file in files:
|
| 1565 |
+
contents = await file.read()
|
| 1566 |
+
file_size = len(contents)
|
| 1567 |
+
|
| 1568 |
+
file_id = next_file_id
|
| 1569 |
+
uploaded_files_db[file_id] = {
|
| 1570 |
+
"id": file_id,
|
| 1571 |
+
"filename": file.filename,
|
| 1572 |
+
"content_type": file.content_type,
|
| 1573 |
+
"size": file_size,
|
| 1574 |
+
"content": base64.b64encode(contents).decode("utf-8")
|
| 1575 |
+
}
|
| 1576 |
+
next_file_id += 1
|
| 1577 |
+
|
| 1578 |
+
uploaded.append({
|
| 1579 |
+
"id": file_id,
|
| 1580 |
+
"filename": file.filename,
|
| 1581 |
+
"content_type": file.content_type,
|
| 1582 |
+
"size": file_size
|
| 1583 |
+
})
|
| 1584 |
+
|
| 1585 |
+
return {
|
| 1586 |
+
"message": f"Uploaded {len(uploaded)} files successfully",
|
| 1587 |
+
"files": uploaded,
|
| 1588 |
+
"total_size": sum(f["size"] for f in uploaded)
|
| 1589 |
+
}
|
| 1590 |
+
|
| 1591 |
+
@app.post("/upload/file-with-data", tags=["Form & File Upload"])
|
| 1592 |
+
async def upload_file_with_form_data(
|
| 1593 |
+
file: UploadFile = File(..., description="File to upload"),
|
| 1594 |
+
title: str = Form(..., description="Title for the file"),
|
| 1595 |
+
description: str = Form(default="", description="Optional description"),
|
| 1596 |
+
category: str = Form(default="general", description="Category")
|
| 1597 |
+
):
|
| 1598 |
+
"""POST - Upload file with additional form fields (multipart/form-data)"""
|
| 1599 |
+
global next_file_id
|
| 1600 |
+
|
| 1601 |
+
contents = await file.read()
|
| 1602 |
+
file_size = len(contents)
|
| 1603 |
+
|
| 1604 |
+
file_id = next_file_id
|
| 1605 |
+
uploaded_files_db[file_id] = {
|
| 1606 |
+
"id": file_id,
|
| 1607 |
+
"filename": file.filename,
|
| 1608 |
+
"content_type": file.content_type,
|
| 1609 |
+
"size": file_size,
|
| 1610 |
+
"content": base64.b64encode(contents).decode("utf-8"),
|
| 1611 |
+
"metadata": {
|
| 1612 |
+
"title": title,
|
| 1613 |
+
"description": description,
|
| 1614 |
+
"category": category
|
| 1615 |
+
}
|
| 1616 |
+
}
|
| 1617 |
+
next_file_id += 1
|
| 1618 |
+
|
| 1619 |
+
return {
|
| 1620 |
+
"message": "File uploaded with metadata",
|
| 1621 |
+
"file": {
|
| 1622 |
+
"id": file_id,
|
| 1623 |
+
"filename": file.filename,
|
| 1624 |
+
"content_type": file.content_type,
|
| 1625 |
+
"size": file_size
|
| 1626 |
+
},
|
| 1627 |
+
"metadata": {
|
| 1628 |
+
"title": title,
|
| 1629 |
+
"description": description,
|
| 1630 |
+
"category": category
|
| 1631 |
+
},
|
| 1632 |
+
"download_url": f"/upload/file/{file_id}"
|
| 1633 |
+
}
|
| 1634 |
+
|
| 1635 |
+
@app.get("/upload/files", tags=["Form & File Upload"])
|
| 1636 |
+
def list_uploaded_files():
|
| 1637 |
+
"""GET - List all uploaded files"""
|
| 1638 |
+
files = [
|
| 1639 |
+
{
|
| 1640 |
+
"id": f["id"],
|
| 1641 |
+
"filename": f["filename"],
|
| 1642 |
+
"content_type": f["content_type"],
|
| 1643 |
+
"size": f["size"],
|
| 1644 |
+
"metadata": f.get("metadata")
|
| 1645 |
+
}
|
| 1646 |
+
for f in uploaded_files_db.values()
|
| 1647 |
+
]
|
| 1648 |
+
return {"files": files, "count": len(files)}
|
| 1649 |
+
|
| 1650 |
+
@app.get("/upload/file/{file_id}", tags=["Form & File Upload"])
|
| 1651 |
+
def download_file(file_id: int = Path(..., description="File ID to download")):
|
| 1652 |
+
"""GET - Download an uploaded file by ID"""
|
| 1653 |
+
if file_id not in uploaded_files_db:
|
| 1654 |
+
raise HTTPException(status_code=404, detail=f"File with id {file_id} not found")
|
| 1655 |
+
|
| 1656 |
+
file_data = uploaded_files_db[file_id]
|
| 1657 |
+
content = base64.b64decode(file_data["content"])
|
| 1658 |
+
|
| 1659 |
+
return Response(
|
| 1660 |
+
content=content,
|
| 1661 |
+
media_type=file_data["content_type"],
|
| 1662 |
+
headers={
|
| 1663 |
+
"Content-Disposition": f'attachment; filename="{file_data["filename"]}"'
|
| 1664 |
+
}
|
| 1665 |
+
)
|
| 1666 |
+
|
| 1667 |
+
@app.delete("/upload/file/{file_id}", tags=["Form & File Upload"])
|
| 1668 |
+
def delete_uploaded_file(file_id: int = Path(..., description="File ID to delete")):
|
| 1669 |
+
"""DELETE - Delete an uploaded file"""
|
| 1670 |
+
if file_id not in uploaded_files_db:
|
| 1671 |
+
raise HTTPException(status_code=404, detail=f"File with id {file_id} not found")
|
| 1672 |
+
|
| 1673 |
+
deleted = uploaded_files_db.pop(file_id)
|
| 1674 |
+
return {
|
| 1675 |
+
"message": "File deleted successfully",
|
| 1676 |
+
"deleted_file": {
|
| 1677 |
+
"id": deleted["id"],
|
| 1678 |
+
"filename": deleted["filename"]
|
| 1679 |
+
}
|
| 1680 |
+
}
|
| 1681 |
+
|
| 1682 |
# ============================================================================
|
| 1683 |
# DIFFERENT RESPONSE FORMATS
|
| 1684 |
# ============================================================================
|