777 lines
16 KiB
Markdown
777 lines
16 KiB
Markdown
# MinIO 存储管理 API 文档
|
||
|
||
## 概述
|
||
|
||
MinIO 存储管理模块提供对象存储的完整管理能力,包括存储桶管理和文件操作。
|
||
|
||
**基础路径**: `/api/v2/storage`
|
||
|
||
**认证要求**: 当前无需认证(后续可按需添加)
|
||
|
||
---
|
||
|
||
## 接口列表
|
||
|
||
| 方法 | 路径 | 说明 |
|
||
|------|------|------|
|
||
| POST | `/buckets` | 创建存储桶 |
|
||
| DELETE | `/buckets` | 删除存储桶 |
|
||
| GET | `/buckets` | 列出所有存储桶 |
|
||
| POST | `/files/copy` | 复制文件 |
|
||
| POST | `/files/move` | 移动/重命名文件 |
|
||
| DELETE | `/files` | 删除单个文件 |
|
||
| POST | `/files/batch-delete` | 批量删除文件 |
|
||
| GET | `/files/download` | 下载文件 |
|
||
| GET | `/files` | 列出目录文件 |
|
||
| GET | `/files/metadata` | 获取文件元数据 |
|
||
| GET | `/files/presigned-url` | 获取预签名URL |
|
||
|
||
---
|
||
|
||
## 存储桶管理
|
||
|
||
### 1. 创建存储桶
|
||
|
||
**POST** `/api/v2/storage/buckets`
|
||
|
||
创建新的 MinIO 存储桶。
|
||
|
||
#### 请求体
|
||
|
||
```json
|
||
{
|
||
"bucket_name": "my-new-bucket"
|
||
}
|
||
```
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|------|------|------|------|
|
||
| bucket_name | string | 是 | 存储桶名称(3-63字符,小写字母、数字、连字符) |
|
||
|
||
#### 响应示例
|
||
|
||
**成功 (200)**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "存储桶创建成功",
|
||
"bucket_name": "my-new-bucket"
|
||
}
|
||
```
|
||
|
||
**存储桶已存在 (200)**
|
||
```json
|
||
{
|
||
"success": false,
|
||
"message": "存储桶已存在: my-new-bucket",
|
||
"bucket_name": "my-new-bucket"
|
||
}
|
||
```
|
||
|
||
#### cURL 示例
|
||
|
||
```bash
|
||
curl -X POST "http://localhost:8000/api/v2/storage/buckets" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"bucket_name": "my-new-bucket"}'
|
||
```
|
||
|
||
---
|
||
|
||
### 2. 删除存储桶
|
||
|
||
**DELETE** `/api/v2/storage/buckets`
|
||
|
||
删除 MinIO 存储桶。支持强制删除(会先删除桶内所有文件)。
|
||
|
||
#### 请求体
|
||
|
||
```json
|
||
{
|
||
"bucket_name": "my-bucket",
|
||
"force": true
|
||
}
|
||
```
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|------|------|------|------|
|
||
| bucket_name | string | 是 | 存储桶名称 |
|
||
| force | boolean | 否 | 强制删除(默认 false,为 true 时会先删除桶内所有文件) |
|
||
|
||
#### 响应示例
|
||
|
||
**成功 (200)**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "存储桶删除成功",
|
||
"bucket_name": "my-bucket",
|
||
"files_deleted": 15
|
||
}
|
||
```
|
||
|
||
**存储桶不存在 (200)**
|
||
```json
|
||
{
|
||
"success": false,
|
||
"message": "存储桶不存在: my-bucket",
|
||
"bucket_name": "my-bucket",
|
||
"files_deleted": 0
|
||
}
|
||
```
|
||
|
||
#### cURL 示例
|
||
|
||
```bash
|
||
# 普通删除(桶必须为空)
|
||
curl -X DELETE "http://localhost:8000/api/v2/storage/buckets" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"bucket_name": "my-bucket"}'
|
||
|
||
# 强制删除(会删除桶内所有文件)
|
||
curl -X DELETE "http://localhost:8000/api/v2/storage/buckets" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"bucket_name": "my-bucket", "force": true}'
|
||
```
|
||
|
||
---
|
||
|
||
### 3. 列出存储桶
|
||
|
||
**GET** `/api/v2/storage/buckets`
|
||
|
||
列出所有 MinIO 存储桶。
|
||
|
||
#### 响应示例
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "共 3 个存储桶",
|
||
"buckets": [
|
||
{
|
||
"name": "docauditai",
|
||
"creation_date": "2024-01-15T08:30:00+00:00"
|
||
},
|
||
{
|
||
"name": "backup",
|
||
"creation_date": "2024-02-20T10:15:00+00:00"
|
||
},
|
||
{
|
||
"name": "temp",
|
||
"creation_date": "2024-03-01T14:00:00+00:00"
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
#### cURL 示例
|
||
|
||
```bash
|
||
curl -X GET "http://localhost:8000/api/v2/storage/buckets"
|
||
```
|
||
|
||
---
|
||
|
||
## 文件操作
|
||
|
||
### 4. 复制文件
|
||
|
||
**POST** `/api/v2/storage/files/copy`
|
||
|
||
在 MinIO 内复制文件。支持跨存储桶复制。
|
||
|
||
#### 请求体
|
||
|
||
```json
|
||
{
|
||
"source_path": "documents/contract.pdf",
|
||
"destination_path": "backup/contract-2024.pdf",
|
||
"source_bucket": null,
|
||
"destination_bucket": null
|
||
}
|
||
```
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|------|------|------|------|
|
||
| source_path | string | 是 | 源文件路径 |
|
||
| destination_path | string | 是 | 目标文件路径 |
|
||
| source_bucket | string | 否 | 源存储桶(默认使用配置的默认桶) |
|
||
| destination_bucket | string | 否 | 目标存储桶(默认使用配置的默认桶) |
|
||
|
||
#### 响应示例
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "文件复制成功",
|
||
"source_path": "docauditai/documents/contract.pdf",
|
||
"destination_path": "docauditai/backup/contract-2024.pdf"
|
||
}
|
||
```
|
||
|
||
#### cURL 示例
|
||
|
||
```bash
|
||
# 同桶内复制
|
||
curl -X POST "http://localhost:8000/api/v2/storage/files/copy" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{
|
||
"source_path": "documents/contract.pdf",
|
||
"destination_path": "backup/contract-2024.pdf"
|
||
}'
|
||
|
||
# 跨桶复制
|
||
curl -X POST "http://localhost:8000/api/v2/storage/files/copy" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{
|
||
"source_path": "documents/contract.pdf",
|
||
"destination_path": "contract.pdf",
|
||
"source_bucket": "docauditai",
|
||
"destination_bucket": "backup"
|
||
}'
|
||
```
|
||
|
||
---
|
||
|
||
### 5. 移动/重命名文件
|
||
|
||
**POST** `/api/v2/storage/files/move`
|
||
|
||
移动文件(复制后删除源文件)。在同一目录内移动即为重命名。
|
||
|
||
#### 请求体
|
||
|
||
```json
|
||
{
|
||
"source_path": "documents/old-name.pdf",
|
||
"destination_path": "documents/new-name.pdf"
|
||
}
|
||
```
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|------|------|------|------|
|
||
| source_path | string | 是 | 源文件路径 |
|
||
| destination_path | string | 是 | 目标文件路径 |
|
||
|
||
#### 响应示例
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "文件移动成功",
|
||
"source_path": "documents/old-name.pdf",
|
||
"destination_path": "documents/new-name.pdf"
|
||
}
|
||
```
|
||
|
||
#### cURL 示例
|
||
|
||
```bash
|
||
# 重命名文件
|
||
curl -X POST "http://localhost:8000/api/v2/storage/files/move" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{
|
||
"source_path": "documents/report-v1.pdf",
|
||
"destination_path": "documents/report-v2.pdf"
|
||
}'
|
||
|
||
# 移动到其他目录
|
||
curl -X POST "http://localhost:8000/api/v2/storage/files/move" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{
|
||
"source_path": "temp/upload.pdf",
|
||
"destination_path": "documents/final.pdf"
|
||
}'
|
||
```
|
||
|
||
---
|
||
|
||
### 6. 删除单个文件
|
||
|
||
**DELETE** `/api/v2/storage/files`
|
||
|
||
删除 MinIO 中的单个文件。
|
||
|
||
#### 请求参数
|
||
|
||
| 参数 | 类型 | 必填 | 说明 |
|
||
|------|------|------|------|
|
||
| file_path | string | 是 | 文件路径 |
|
||
|
||
#### 响应示例
|
||
|
||
**成功 (200)**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "文件删除成功",
|
||
"file_path": "documents/old-file.pdf"
|
||
}
|
||
```
|
||
|
||
**文件不存在 (200)**
|
||
```json
|
||
{
|
||
"success": false,
|
||
"message": "文件不存在: documents/old-file.pdf",
|
||
"file_path": "documents/old-file.pdf"
|
||
}
|
||
```
|
||
|
||
#### cURL 示例
|
||
|
||
```bash
|
||
curl -X DELETE "http://localhost:8000/api/v2/storage/files?file_path=documents/old-file.pdf"
|
||
```
|
||
|
||
---
|
||
|
||
### 7. 批量删除文件
|
||
|
||
**POST** `/api/v2/storage/files/batch-delete`
|
||
|
||
批量删除多个文件。
|
||
|
||
#### 请求体
|
||
|
||
```json
|
||
{
|
||
"file_paths": [
|
||
"temp/file1.pdf",
|
||
"temp/file2.pdf",
|
||
"temp/file3.pdf"
|
||
]
|
||
}
|
||
```
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|------|------|------|------|
|
||
| file_paths | string[] | 是 | 文件路径列表(至少1个) |
|
||
|
||
#### 响应示例
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "删除完成: 成功 3, 失败 0",
|
||
"deleted_count": 3,
|
||
"failed_count": 0,
|
||
"failed_paths": []
|
||
}
|
||
```
|
||
|
||
**部分失败 (200)**
|
||
```json
|
||
{
|
||
"success": false,
|
||
"message": "删除完成: 成功 2, 失败 1",
|
||
"deleted_count": 2,
|
||
"failed_count": 1,
|
||
"failed_paths": ["temp/not-exist.pdf"]
|
||
}
|
||
```
|
||
|
||
#### cURL 示例
|
||
|
||
```bash
|
||
curl -X POST "http://localhost:8000/api/v2/storage/files/batch-delete" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{
|
||
"file_paths": [
|
||
"temp/file1.pdf",
|
||
"temp/file2.pdf",
|
||
"temp/file3.pdf"
|
||
]
|
||
}'
|
||
```
|
||
|
||
---
|
||
|
||
### 8. 下载文件
|
||
|
||
**GET** `/api/v2/storage/files/download`
|
||
|
||
从 MinIO 下载文件。返回文件流。
|
||
|
||
#### 请求参数
|
||
|
||
| 参数 | 类型 | 必填 | 说明 |
|
||
|------|------|------|------|
|
||
| file_path | string | 是 | 文件路径 |
|
||
| filename | string | 否 | 下载时显示的文件名 |
|
||
|
||
#### 响应
|
||
|
||
返回文件二进制流,包含以下响应头:
|
||
|
||
```
|
||
Content-Type: application/pdf
|
||
Content-Disposition: attachment; filename="contract.pdf"
|
||
Content-Length: 1234567
|
||
```
|
||
|
||
#### cURL 示例
|
||
|
||
```bash
|
||
# 下载文件
|
||
curl -X GET "http://localhost:8000/api/v2/storage/files/download?file_path=documents/contract.pdf" \
|
||
-o contract.pdf
|
||
|
||
# 指定下载文件名
|
||
curl -X GET "http://localhost:8000/api/v2/storage/files/download?file_path=documents/contract.pdf&filename=my-contract.pdf" \
|
||
-o my-contract.pdf
|
||
```
|
||
|
||
#### 浏览器直接下载
|
||
|
||
```
|
||
http://localhost:8000/api/v2/storage/files/download?file_path=documents/contract.pdf
|
||
```
|
||
|
||
---
|
||
|
||
### 9. 列出目录文件
|
||
|
||
**GET** `/api/v2/storage/files`
|
||
|
||
列出指定目录下的文件。
|
||
|
||
#### 请求参数
|
||
|
||
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|
||
|------|------|------|--------|------|
|
||
| directory_path | string | 否 | "" | 目录路径 |
|
||
| recursive | boolean | 否 | false | 是否递归列出子目录 |
|
||
| max_files | int | 否 | 100 | 最大返回数量(1-1000) |
|
||
| marker | string | 否 | null | 分页标记 |
|
||
|
||
#### 响应示例
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "共 5 个文件",
|
||
"files": [
|
||
"documents/contract-001.pdf",
|
||
"documents/contract-002.pdf",
|
||
"documents/contract-003.pdf",
|
||
"documents/report.docx",
|
||
"documents/summary.xlsx"
|
||
],
|
||
"total_count": 5,
|
||
"next_marker": null
|
||
}
|
||
```
|
||
|
||
**分页响应 (200)**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "共 100 个文件",
|
||
"files": ["..."],
|
||
"total_count": 100,
|
||
"next_marker": "documents/file-100.pdf"
|
||
}
|
||
```
|
||
|
||
#### cURL 示例
|
||
|
||
```bash
|
||
# 列出根目录
|
||
curl -X GET "http://localhost:8000/api/v2/storage/files"
|
||
|
||
# 列出指定目录
|
||
curl -X GET "http://localhost:8000/api/v2/storage/files?directory_path=documents"
|
||
|
||
# 递归列出
|
||
curl -X GET "http://localhost:8000/api/v2/storage/files?directory_path=documents&recursive=true"
|
||
|
||
# 分页查询
|
||
curl -X GET "http://localhost:8000/api/v2/storage/files?directory_path=documents&max_files=50"
|
||
|
||
# 获取下一页
|
||
curl -X GET "http://localhost:8000/api/v2/storage/files?directory_path=documents&max_files=50&marker=documents/file-50.pdf"
|
||
```
|
||
|
||
---
|
||
|
||
### 10. 获取文件元数据
|
||
|
||
**GET** `/api/v2/storage/files/metadata`
|
||
|
||
获取文件的详细元数据信息。
|
||
|
||
#### 请求参数
|
||
|
||
| 参数 | 类型 | 必填 | 说明 |
|
||
|------|------|------|------|
|
||
| file_path | string | 是 | 文件路径 |
|
||
|
||
#### 响应示例
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "获取成功",
|
||
"file_info": {
|
||
"path": "documents/contract.pdf",
|
||
"size": 1234567,
|
||
"content_type": "application/pdf",
|
||
"created_time": "2024-03-15T10:30:00+00:00",
|
||
"modified_time": "2024-03-15T10:30:00+00:00",
|
||
"etag": "\"d41d8cd98f00b204e9800998ecf8427e\"",
|
||
"url": "http://minio:9000/docauditai/documents/contract.pdf?X-Amz-..."
|
||
}
|
||
}
|
||
```
|
||
|
||
#### cURL 示例
|
||
|
||
```bash
|
||
curl -X GET "http://localhost:8000/api/v2/storage/files/metadata?file_path=documents/contract.pdf"
|
||
```
|
||
|
||
---
|
||
|
||
### 11. 获取预签名URL
|
||
|
||
**GET** `/api/v2/storage/files/presigned-url`
|
||
|
||
获取文件的预签名下载URL,可用于临时分享。
|
||
|
||
#### 请求参数
|
||
|
||
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|
||
|------|------|------|--------|------|
|
||
| file_path | string | 是 | - | 文件路径 |
|
||
| expires | int | 否 | 3600 | 过期时间(秒),范围 60-604800(7天) |
|
||
|
||
#### 响应示例
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "获取成功",
|
||
"url": "http://minio:9000/docauditai/documents/contract.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=...",
|
||
"expires_in": 3600
|
||
}
|
||
```
|
||
|
||
#### cURL 示例
|
||
|
||
```bash
|
||
# 默认1小时过期
|
||
curl -X GET "http://localhost:8000/api/v2/storage/files/presigned-url?file_path=documents/contract.pdf"
|
||
|
||
# 自定义过期时间(1天)
|
||
curl -X GET "http://localhost:8000/api/v2/storage/files/presigned-url?file_path=documents/contract.pdf&expires=86400"
|
||
|
||
# 最长7天
|
||
curl -X GET "http://localhost:8000/api/v2/storage/files/presigned-url?file_path=documents/contract.pdf&expires=604800"
|
||
```
|
||
|
||
---
|
||
|
||
## 前端集成示例
|
||
|
||
### JavaScript/Fetch
|
||
|
||
```javascript
|
||
const API_BASE = 'http://localhost:8000/api/v2/storage';
|
||
|
||
// 列出存储桶
|
||
async function listBuckets() {
|
||
const response = await fetch(`${API_BASE}/buckets`);
|
||
return response.json();
|
||
}
|
||
|
||
// 创建存储桶
|
||
async function createBucket(bucketName) {
|
||
const response = await fetch(`${API_BASE}/buckets`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ bucket_name: bucketName })
|
||
});
|
||
return response.json();
|
||
}
|
||
|
||
// 复制文件
|
||
async function copyFile(sourcePath, destPath) {
|
||
const response = await fetch(`${API_BASE}/files/copy`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
source_path: sourcePath,
|
||
destination_path: destPath
|
||
})
|
||
});
|
||
return response.json();
|
||
}
|
||
|
||
// 重命名文件
|
||
async function renameFile(oldPath, newPath) {
|
||
const response = await fetch(`${API_BASE}/files/move`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
source_path: oldPath,
|
||
destination_path: newPath
|
||
})
|
||
});
|
||
return response.json();
|
||
}
|
||
|
||
// 删除文件
|
||
async function deleteFile(filePath) {
|
||
const response = await fetch(
|
||
`${API_BASE}/files?file_path=${encodeURIComponent(filePath)}`,
|
||
{ method: 'DELETE' }
|
||
);
|
||
return response.json();
|
||
}
|
||
|
||
// 下载文件
|
||
function downloadFile(filePath, filename) {
|
||
const url = `${API_BASE}/files/download?file_path=${encodeURIComponent(filePath)}`;
|
||
if (filename) {
|
||
url += `&filename=${encodeURIComponent(filename)}`;
|
||
}
|
||
window.open(url, '_blank');
|
||
}
|
||
|
||
// 获取文件列表
|
||
async function listFiles(directory = '', recursive = false) {
|
||
const params = new URLSearchParams({
|
||
directory_path: directory,
|
||
recursive: recursive.toString()
|
||
});
|
||
const response = await fetch(`${API_BASE}/files?${params}`);
|
||
return response.json();
|
||
}
|
||
|
||
// 获取预签名URL
|
||
async function getPresignedUrl(filePath, expires = 3600) {
|
||
const params = new URLSearchParams({
|
||
file_path: filePath,
|
||
expires: expires.toString()
|
||
});
|
||
const response = await fetch(`${API_BASE}/files/presigned-url?${params}`);
|
||
return response.json();
|
||
}
|
||
```
|
||
|
||
### Vue 3 组件示例
|
||
|
||
```vue
|
||
<template>
|
||
<div class="file-manager">
|
||
<div class="file-list">
|
||
<div v-for="file in files" :key="file" class="file-item">
|
||
<span>{{ file }}</span>
|
||
<div class="actions">
|
||
<button @click="handleDownload(file)">下载</button>
|
||
<button @click="handleRename(file)">重命名</button>
|
||
<button @click="handleDelete(file)">删除</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, onMounted } from 'vue';
|
||
|
||
const API_BASE = '/api/v2/storage';
|
||
const files = ref([]);
|
||
|
||
onMounted(async () => {
|
||
await loadFiles();
|
||
});
|
||
|
||
async function loadFiles() {
|
||
const response = await fetch(`${API_BASE}/files?directory_path=documents&recursive=true`);
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
files.value = data.files;
|
||
}
|
||
}
|
||
|
||
function handleDownload(filePath) {
|
||
window.open(`${API_BASE}/files/download?file_path=${encodeURIComponent(filePath)}`);
|
||
}
|
||
|
||
async function handleRename(filePath) {
|
||
const newName = prompt('请输入新文件名:');
|
||
if (!newName) return;
|
||
|
||
const directory = filePath.substring(0, filePath.lastIndexOf('/'));
|
||
const newPath = `${directory}/${newName}`;
|
||
|
||
const response = await fetch(`${API_BASE}/files/move`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
source_path: filePath,
|
||
destination_path: newPath
|
||
})
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
await loadFiles();
|
||
} else {
|
||
alert(data.message);
|
||
}
|
||
}
|
||
|
||
async function handleDelete(filePath) {
|
||
if (!confirm(`确定删除 ${filePath}?`)) return;
|
||
|
||
const response = await fetch(
|
||
`${API_BASE}/files?file_path=${encodeURIComponent(filePath)}`,
|
||
{ method: 'DELETE' }
|
||
);
|
||
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
await loadFiles();
|
||
} else {
|
||
alert(data.message);
|
||
}
|
||
}
|
||
</script>
|
||
```
|
||
|
||
---
|
||
|
||
## 错误处理
|
||
|
||
所有接口返回统一格式:
|
||
|
||
```json
|
||
{
|
||
"success": false,
|
||
"message": "错误描述信息"
|
||
}
|
||
```
|
||
|
||
### 常见错误
|
||
|
||
| 错误 | 说明 | 解决方案 |
|
||
|------|------|----------|
|
||
| 存储桶不存在 | 操作的存储桶未找到 | 检查桶名是否正确 |
|
||
| 文件不存在 | 操作的文件路径无效 | 检查文件路径 |
|
||
| 存储桶非空 | 删除桶时桶内有文件 | 使用 force=true 强制删除 |
|
||
| 权限不足 | MinIO 访问凭证问题 | 检查配置的 access_key/secret_key |
|
||
|
||
---
|
||
|
||
## 测试
|
||
|
||
使用测试脚本进行完整测试:
|
||
|
||
```bash
|
||
python scripts/test_minio_api.py --host localhost --port 8000
|
||
```
|