init commit

This commit is contained in:
2025-06-05 09:17:47 -04:00
commit db8ec76921
53 changed files with 12126 additions and 0 deletions

16
templates/base.html.twig Normal file
View File

@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{% block title %}Welcome!{% endblock %}</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>⚫️</text><text y=%221.3em%22 x=%220.2em%22 font-size=%2276%22 fill=%22%23fff%22>sf</text></svg>">
{% block stylesheets %}
{% endblock %}
{% block javascripts %}
{% endblock %}
</head>
<body>
{% block body %}{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,864 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>vStash - Object Storage Management Console</title>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f8fafc;
color: #1a202c;
}
.container {
display: flex;
min-height: 100vh;
}
.sidebar {
width: 250px;
background: #2d3748;
color: white;
padding: 1rem 0;
}
.sidebar h1 {
padding: 0 1rem 2rem;
font-size: 1.5rem;
border-bottom: 1px solid #4a5568;
margin-bottom: 1rem;
}
.nav-item {
display: block;
padding: 0.75rem 1rem;
color: #e2e8f0;
text-decoration: none;
transition: background 0.2s;
cursor: pointer;
}
.nav-item:hover, .nav-item.active {
background: #4a5568;
color: white;
}
.nav-item i {
width: 20px;
margin-right: 0.5rem;
}
.main-content {
flex: 1;
padding: 2rem;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 1px solid #e2e8f0;
}
.btn {
padding: 0.5rem 1rem;
border: none;
border-radius: 0.375rem;
cursor: pointer;
font-size: 0.875rem;
transition: all 0.2s;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.btn-primary {
background: #3182ce;
color: white;
}
.btn-primary:hover {
background: #2c5282;
}
.btn-danger {
background: #e53e3e;
color: white;
}
.btn-danger:hover {
background: #c53030;
}
.btn-secondary {
background: #718096;
color: white;
}
.btn-secondary:hover {
background: #4a5568;
}
.btn-sm {
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
}
.card {
background: white;
border-radius: 0.5rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
margin-bottom: 1rem;
}
.card-header {
padding: 1rem;
border-bottom: 1px solid #e2e8f0;
font-weight: 600;
display: flex;
justify-content: space-between;
align-items: center;
}
.card-body {
padding: 1rem;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.stat-card {
background: white;
padding: 1.5rem;
border-radius: 0.5rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
text-align: center;
}
.stat-number {
font-size: 2rem;
font-weight: bold;
color: #3182ce;
display: block;
}
.stat-label {
color: #718096;
font-size: 0.875rem;
margin-top: 0.5rem;
}
.table {
width: 100%;
border-collapse: collapse;
}
.table th,
.table td {
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid #e2e8f0;
}
.table th {
background: #f7fafc;
font-weight: 600;
}
.table tbody tr:hover {
background: #f7fafc;
}
.badge {
padding: 0.25rem 0.5rem;
border-radius: 0.375rem;
font-size: 0.75rem;
font-weight: 600;
}
.badge-success {
background: #c6f6d5;
color: #22543d;
}
.badge-danger {
background: #fed7d7;
color: #742a2a;
}
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: white;
border-radius: 0.5rem;
padding: 2rem;
max-width: 500px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
}
.form-group {
margin-bottom: 1rem;
}
.form-label {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
}
.form-input,
.form-select {
width: 100%;
padding: 0.5rem;
border: 1px solid #d2d6dc;
border-radius: 0.375rem;
font-size: 0.875rem;
}
.form-input:focus,
.form-select:focus {
outline: none;
border-color: #3182ce;
box-shadow: 0 0 0 3px rgba(49, 130, 206, 0.1);
}
.loading {
text-align: center;
padding: 2rem;
color: #718096;
}
.error {
background: #fed7d7;
color: #742a2a;
padding: 1rem;
border-radius: 0.375rem;
margin-bottom: 1rem;
}
.success {
background: #c6f6d5;
color: #22543d;
padding: 1rem;
border-radius: 0.375rem;
margin-bottom: 1rem;
}
.empty-state {
text-align: center;
padding: 3rem 1rem;
color: #718096;
}
.empty-state i {
font-size: 3rem;
margin-bottom: 1rem;
opacity: 0.5;
}
.search-box {
margin-bottom: 1rem;
}
.search-input {
width: 100%;
max-width: 300px;
padding: 0.5rem 1rem;
border: 1px solid #d2d6dc;
border-radius: 0.375rem;
}
.actions {
display: flex;
gap: 0.5rem;
}
.fade-in {
animation: fadeIn 0.3s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
[x-cloak] { display: none !important; }
</style>
</head>
<body>
<div x-data="consoleApp()" x-init="init()" class="container">
<!-- Sidebar -->
<nav class="sidebar">
<h1><i class="fas fa-cloud"></i> vStash</h1>
<a @click="setActiveSection('dashboard')" class="nav-item" :class="{ active: activeSection === 'dashboard' }">
<i class="fas fa-chart-bar"></i> Dashboard
</a>
<a @click="setActiveSection('credentials')" class="nav-item" :class="{ active: activeSection === 'credentials' }">
<i class="fas fa-key"></i> Credentials
</a>
<a @click="setActiveSection('buckets')" class="nav-item" :class="{ active: activeSection === 'buckets' }">
<i class="fas fa-folder"></i> Buckets
</a>
<a @click="setActiveSection('multipart')" class="nav-item" :class="{ active: activeSection === 'multipart' }">
<i class="fas fa-upload"></i> Multipart Uploads
</a>
<a @click="setActiveSection('presigned')" class="nav-item" :class="{ active: activeSection === 'presigned' }">
<i class="fas fa-link"></i> Presigned URLs
</a>
</nav>
<!-- Main Content -->
<main class="main-content">
<!-- Dashboard Section -->
<div x-show="activeSection === 'dashboard'" x-cloak class="fade-in">
<div class="header">
<h2><i class="fas fa-chart-bar"></i> Dashboard</h2>
<button @click="loadStats()" class="btn btn-secondary">
<i class="fas fa-sync-alt"></i> Refresh
</button>
</div>
<div x-show="loading" class="loading">
<i class="fas fa-spinner fa-spin fa-2x"></i>
<p>Loading statistics...</p>
</div>
<div x-show="!loading && stats" class="stats-grid">
<div class="stat-card">
<span class="stat-number" x-text="stats.credentials"></span>
<div class="stat-label">Credentials</div>
</div>
<div class="stat-card">
<span class="stat-number" x-text="stats.buckets"></span>
<div class="stat-label">Buckets</div>
</div>
<div class="stat-card">
<span class="stat-number" x-text="stats.objects"></span>
<div class="stat-label">Objects</div>
</div>
<div class="stat-card">
<span class="stat-number" x-text="stats.total_storage_human"></span>
<div class="stat-label">Total Storage</div>
</div>
<div class="stat-card">
<span class="stat-number" x-text="stats.active_multipart_uploads"></span>
<div class="stat-label">Active Uploads</div>
</div>
<div class="stat-card">
<span class="stat-number" x-text="stats.active_presigned_urls"></span>
<div class="stat-label">Active URLs</div>
</div>
</div>
</div>
<!-- Credentials Section -->
<div x-show="activeSection === 'credentials'" x-cloak class="fade-in">
<div class="header">
<h2><i class="fas fa-key"></i> Access Credentials</h2>
<button @click="showCreateCredentialModal = true" class="btn btn-primary">
<i class="fas fa-plus"></i> Create Credential
</button>
</div>
<div x-show="message" x-text="message" :class="messageType === 'error' ? 'error' : 'success'"></div>
<div class="card">
<div class="card-body">
<div x-show="loading" class="loading">Loading credentials...</div>
<template x-if="!loading && credentials.length === 0">
<div class="empty-state">
<i class="fas fa-key"></i>
<h3>No credentials found</h3>
<p>Create your first access credential to get started.</p>
</div>
</template>
<template x-if="!loading && credentials.length > 0">
<table class="table">
<thead>
<tr>
<th>Access Key</th>
<th>User Name</th>
<th>Status</th>
<th>Buckets</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<template x-for="credential in credentials" :key="credential.id">
<tr>
<td>
<code x-text="credential.access_key"></code>
</td>
<td x-text="credential.user_name || '-'"></td>
<td>
<span class="badge" :class="credential.is_active ? 'badge-success' : 'badge-danger'"
x-text="credential.is_active ? 'Active' : 'Inactive'"></span>
</td>
<td x-text="credential.bucket_count"></td>
<td x-text="credential.created_at"></td>
<td>
<div class="actions">
<button @click="viewCredential(credential)" class="btn btn-sm btn-secondary">
<i class="fas fa-eye"></i>
</button>
<button @click="deleteCredential(credential.id)" class="btn btn-sm btn-danger">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
</template>
</tbody>
</table>
</template>
</div>
</div>
</div>
<!-- Buckets Section -->
<div x-show="activeSection === 'buckets'" x-cloak class="fade-in">
<div class="header">
<h2><i class="fas fa-folder"></i> Storage Buckets</h2>
<button @click="showCreateBucketModal = true; loadCredentials()" class="btn btn-primary">
<i class="fas fa-plus"></i> Create Bucket
</button>
</div>
<div x-show="message" x-text="message" :class="messageType === 'error' ? 'error' : 'success'"></div>
<div class="card">
<div class="card-body">
<div x-show="loading" class="loading">Loading buckets...</div>
<template x-if="!loading && buckets.length === 0">
<div class="empty-state">
<i class="fas fa-folder"></i>
<h3>No buckets found</h3>
<p>Create your first bucket to start storing objects.</p>
</div>
</template>
<template x-if="!loading && buckets.length > 0">
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Region</th>
<th>Owner</th>
<th>Objects</th>
<th>Size</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<template x-for="bucket in buckets" :key="bucket.name">
<tr>
<td>
<a @click="viewBucket(bucket.name)" class="text-blue-600 cursor-pointer" x-text="bucket.name"></a>
</td>
<td x-text="bucket.region"></td>
<td x-text="bucket.owner"></td>
<td x-text="bucket.object_count"></td>
<td x-text="bucket.total_size_human"></td>
<td x-text="bucket.created_at"></td>
<td>
<div class="actions">
<button @click="viewBucket(bucket.name)" class="btn btn-sm btn-secondary">
<i class="fas fa-eye"></i>
</button>
<button @click="deleteBucket(bucket.name)" class="btn btn-sm btn-danger">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
</template>
</tbody>
</table>
</template>
</div>
</div>
</div>
<!-- Bucket Details Section -->
<div x-show="activeSection === 'bucket-detail'" x-cloak class="fade-in">
<div class="header">
<div>
<button @click="setActiveSection('buckets')" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Back
</button>
<h2 style="display: inline-block; margin-left: 1rem;">
<i class="fas fa-folder"></i> <span x-text="selectedBucket?.name"></span>
</h2>
</div>
</div>
<div x-show="selectedBucket" class="card">
<div class="card-header">
Objects
<div>
<input x-model="objectSearch" @input="filterObjects()"
placeholder="Search objects..." class="search-input">
</div>
</div>
<div class="card-body">
<div x-show="loading" class="loading">Loading objects...</div>
<template x-if="!loading && filteredObjects.length === 0">
<div class="empty-state">
<i class="fas fa-file"></i>
<h3>No objects found</h3>
<p>This bucket is empty or no objects match your search.</p>
</div>
</template>
<template x-if="!loading && filteredObjects.length > 0">
<table class="table">
<thead>
<tr>
<th>Key</th>
<th>Size</th>
<th>Type</th>
<th>Modified</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<template x-for="object in filteredObjects" :key="object.key">
<tr>
<td x-text="object.key"></td>
<td x-text="object.size_human"></td>
<td x-text="object.content_type"></td>
<td x-text="object.created_at"></td>
<td>
<button @click="deleteObject(selectedBucket.name, object.key)" class="btn btn-sm btn-danger">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
</template>
</tbody>
</table>
</template>
</div>
</div>
</div>
<!-- Create Credential Modal -->
<div x-show="showCreateCredentialModal" x-cloak class="modal" @click.self="showCreateCredentialModal = false">
<div class="modal-content">
<h3 style="margin-bottom: 1rem;">Create New Credential</h3>
<form @submit.prevent="createCredential()">
<div class="form-group">
<label class="form-label">User Name</label>
<input x-model="newCredential.user_name" type="text" class="form-input" placeholder="Optional">
</div>
<div class="form-group">
<label class="form-label">Access Key</label>
<input x-model="newCredential.access_key" type="text" class="form-input"
placeholder="Leave blank to auto-generate">
</div>
<div class="form-group">
<label class="form-label">Secret Key</label>
<input x-model="newCredential.secret_key" type="text" class="form-input"
placeholder="Leave blank to auto-generate">
</div>
<div style="display: flex; gap: 1rem; justify-content: flex-end;">
<button type="button" @click="showCreateCredentialModal = false" class="btn btn-secondary">
Cancel
</button>
<button type="submit" class="btn btn-primary">
Create
</button>
</div>
</form>
</div>
</div>
<!-- Create Bucket Modal -->
<div x-show="showCreateBucketModal" x-cloak class="modal" @click.self="showCreateBucketModal = false">
<div class="modal-content">
<h3 style="margin-bottom: 1rem;">Create New Bucket</h3>
<form @submit.prevent="createBucket()">
<div class="form-group">
<label class="form-label">Bucket Name</label>
<input x-model="newBucket.name" type="text" class="form-input"
placeholder="my-bucket-name" required>
</div>
<div class="form-group">
<label class="form-label">Owner</label>
<select x-model="newBucket.owner_id" class="form-select" required>
<option value="">Select owner...</option>
<template x-for="credential in credentials" :key="credential.id">
<option :value="credential.id" x-text="credential.user_name || credential.access_key"></option>
</template>
</select>
</div>
<div class="form-group">
<label class="form-label">Region</label>
<select x-model="newBucket.region" class="form-select">
<option value="us-east-1">us-east-1</option>
<option value="us-west-2">us-west-2</option>
<option value="eu-west-1">eu-west-1</option>
</select>
</div>
<div style="display: flex; gap: 1rem; justify-content: flex-end;">
<button type="button" @click="showCreateBucketModal = false" class="btn btn-secondary">
Cancel
</button>
<button type="submit" class="btn btn-primary">
Create
</button>
</div>
</form>
</div>
</div>
</main>
</div>
<script>
function consoleApp() {
return {
activeSection: 'dashboard',
loading: false,
message: '',
messageType: 'success',
// Data
stats: null,
credentials: [],
buckets: [],
selectedBucket: null,
filteredObjects: [],
objectSearch: '',
// Modals
showCreateCredentialModal: false,
showCreateBucketModal: false,
// Forms
newCredential: {
user_name: '',
access_key: '',
secret_key: ''
},
newBucket: {
name: '',
owner_id: '',
region: 'us-east-1'
},
init() {
this.loadStats();
},
setActiveSection(section) {
this.activeSection = section;
this.message = '';
switch(section) {
case 'dashboard':
this.loadStats();
break;
case 'credentials':
this.loadCredentials();
break;
case 'buckets':
this.loadBuckets();
break;
}
},
async apiCall(url, options = {}) {
try {
const response = await fetch(`/api${url}`, {
headers: {
'Content-Type': 'application/json',
...options.headers
},
...options
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
this.showMessage('API Error: ' + error.message, 'error');
throw error;
}
},
showMessage(message, type = 'success') {
this.message = message;
this.messageType = type;
setTimeout(() => {
this.message = '';
}, 5000);
},
async loadStats() {
this.loading = true;
try {
this.stats = await this.apiCall('/stats');
} catch (error) {
console.error('Failed to load stats:', error);
}
this.loading = false;
},
async loadCredentials() {
this.loading = true;
try {
const response = await this.apiCall('/credentials');
this.credentials = response.credentials;
} catch (error) {
console.error('Failed to load credentials:', error);
}
this.loading = false;
},
async loadBuckets() {
this.loading = true;
try {
const response = await this.apiCall('/buckets');
this.buckets = response.buckets;
} catch (error) {
console.error('Failed to load buckets:', error);
}
this.loading = false;
},
async createCredential() {
try {
await this.apiCall('/credentials', {
method: 'POST',
body: JSON.stringify(this.newCredential)
});
this.showMessage('Credential created successfully!');
this.showCreateCredentialModal = false;
this.newCredential = { user_name: '', access_key: '', secret_key: '' };
this.loadCredentials();
} catch (error) {
console.error('Failed to create credential:', error);
}
},
async createBucket() {
try {
await this.apiCall('/buckets', {
method: 'POST',
body: JSON.stringify(this.newBucket)
});
this.showMessage('Bucket created successfully!');
this.showCreateBucketModal = false;
this.newBucket = { name: '', owner_id: '', region: 'us-east-1' };
this.loadBuckets();
} catch (error) {
console.error('Failed to create bucket:', error);
}
},
async deleteCredential(id) {
if (!confirm('Are you sure you want to delete this credential?')) return;
try {
await this.apiCall(`/credentials/${id}`, { method: 'DELETE' });
this.showMessage('Credential deleted successfully!');
this.loadCredentials();
} catch (error) {
console.error('Failed to delete credential:', error);
}
},
async deleteBucket(name) {
if (!confirm('Are you sure you want to delete this bucket?')) return;
try {
await this.apiCall(`/buckets/${name}`, { method: 'DELETE' });
this.showMessage('Bucket deleted successfully!');
this.loadBuckets();
} catch (error) {
console.error('Failed to delete bucket:', error);
}
},
async viewBucket(bucketName) {
this.loading = true;
try {
this.selectedBucket = await this.apiCall(`/buckets/${bucketName}`);
this.filteredObjects = this.selectedBucket.objects || [];
this.activeSection = 'bucket-detail';
} catch (error) {
console.error('Failed to load bucket details:', error);
}
this.loading = false;
},
filterObjects() {
if (!this.selectedBucket) return;
this.filteredObjects = this.selectedBucket.objects.filter(obj =>
obj.key.toLowerCase().includes(this.objectSearch.toLowerCase())
);
},
async deleteObject(bucketName, objectKey) {
if (!confirm('Are you sure you want to delete this object?')) return;
try {
await this.apiCall(`/buckets/${bucketName}/objects/${encodeURIComponent(objectKey)}`, {
method: 'DELETE'
});
this.showMessage('Object deleted successfully!');
this.viewBucket(bucketName);
} catch (error) {
console.error('Failed to delete object:', error);
}
},
async viewCredential(credential) {
// Could open a modal with full credential details
alert(`Access Key: ${credential.access_key}\nUser: ${credential.user_name || 'N/A'}\nStatus: ${credential.is_active ? 'Active' : 'Inactive'}`);
}
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,5 @@
apis:
main:
root: ./openapi.yaml
rules:
no-unused-components: off

View File

@@ -0,0 +1,120 @@
#!/bin/bash
# vStash OpenAPI Documentation Generator
# This script compiles the OpenAPI YAML files and generates HTML documentation
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
echo -e "${BLUE}🚀 vStash API Documentation Generator${NC}"
echo "================================================"
# Check if we're in the right directory
if [ ! -f "templates/openapi/v1.yaml" ]; then
echo -e "${RED}❌ Error: templates/openapi/v1.yaml not found${NC}"
echo "Please run this script from the project root directory"
exit 1
fi
cd templates/openapi
# Check for required tools
echo -e "${BLUE}🔍 Checking dependencies...${NC}"
if ! command -v redocly &> /dev/null; then
echo -e "${YELLOW}⚠️ redocly not found. Installing...${NC}"
npm install -g @redocly/cli@latest
fi
# Try to install modern swagger tools
if ! command -v swagger-cli &> /dev/null; then
echo -e "${YELLOW}⚠️ swagger-cli not found. Installing modern version...${NC}"
npm install -g @apidevtools/swagger-cli
fi
echo -e "${GREEN}✅ Dependencies check complete${NC}"
# Create output directory
mkdir -p html
# Step 1: Bundle YAML files
echo -e "${BLUE}📦 Bundling YAML files...${NC}"
# Try redocly first (better multi-file support)
if redocly bundle v1.yaml --output openapi.yaml; then
echo -e "${GREEN}✅ YAML bundling complete (using redocly)${NC}"
fi
# Step 2: Validate the bundled YAML
echo -e "${BLUE}🔍 Validating OpenAPI specification...${NC}"
redocly lint openapi.yaml
if [ $? -eq 0 ]; then
echo -e "${GREEN}✅ OpenAPI validation passed${NC}"
else
echo -e "${YELLOW}⚠️ OpenAPI validation warnings (proceeding anyway)${NC}"
fi
# Step 3: Generate HTML documentation
echo -e "${BLUE}📖 Generating HTML documentation...${NC}"
redocly build-docs openapi.yaml --output=html/index.html
if [ $? -eq 0 ]; then
echo -e "${GREEN}✅ HTML documentation generated${NC}"
else
echo -e "${RED}❌ HTML documentation generation failed${NC}"
exit 1
fi
# Step 4: Generate additional formats (optional)
echo -e "${BLUE}📄 Generating additional documentation formats...${NC}"
# Generate a cleaner JSON version
if command -v yq &> /dev/null; then
yq eval openapi.yaml -o=json > openapi.json
echo -e "${GREEN}✅ JSON format generated${NC}"
else
echo -e "${YELLOW}⚠️ yq not found, skipping JSON generation${NC}"
fi
echo ""
echo -e "${GREEN}🎉 Documentation generation complete!${NC}"
echo "================================================"
echo -e "${BLUE}📁 Generated files:${NC}"
echo " • templates/openapi/openapi.yaml (bundled spec)"
echo " • templates/openapi/openapi.json (JSON format)"
echo " • templates/openapi/html/index.html (main docs)"
echo ""
echo -e "${BLUE}🌐 To view the documentation:${NC}"
echo " Local: file://$(pwd)/html/index.html"
echo " Or serve with: python -m http.server 8080 -d html"
echo ""
echo -e "${BLUE}📋 Next steps:${NC}"
echo " 1. Review the generated documentation"
echo " 2. Deploy html/index.html to your web server"
echo " 3. Update API endpoints if needed"
echo " 4. Share with your team!"
echo ""
# Optional: Open documentation in browser (macOS/Linux)
if command -v open &> /dev/null; then
read -p "Open documentation in browser? (y/n): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
open html/index.html
fi
elif command -v xdg-open &> /dev/null; then
read -p "Open documentation in browser? (y/n): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
xdg-open html/index.html
fi
fi
echo -e "${GREEN}✨ All done!${NC}"

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,266 @@
BucketName:
name: bucket
in: path
required: true
description: Name of the S3 bucket
schema:
type: string
pattern: '^[a-z0-9\-\.]+$'
minLength: 3
maxLength: 63
example: "my-bucket"
ObjectKey:
name: key
in: path
required: true
description: Object key (file path) within the bucket
schema:
type: string
maxLength: 255
example: "folder/file.txt"
style: simple
explode: false
CredentialId:
name: id
in: path
required: true
description: Unique identifier for the credential
schema:
type: integer
minimum: 1
example: 1
Prefix:
name: prefix
in: query
description: Limits the response to keys that begin with the specified prefix
schema:
type: string
maxLength: 255
example: "photos/"
Marker:
name: marker
in: query
description: Specifies the key to start with when listing objects
schema:
type: string
maxLength: 255
example: "photos/2023/"
MaxKeys:
name: max-keys
in: query
description: Maximum number of keys to return
schema:
type: integer
minimum: 1
maximum: 1000
default: 1000
example: 100
Delimiter:
name: delimiter
in: query
description: Character used to group keys
schema:
type: string
maxLength: 1
example: "/"
PartNumber:
name: partNumber
in: query
description: Part number for multipart upload (1-10,000)
schema:
type: integer
minimum: 1
maximum: 10000
example: 1
UploadId:
name: uploadId
in: query
description: Upload ID for multipart upload operations
schema:
type: string
maxLength: 64
example: "upload_12345_abcdef"
VersionId:
name: versionId
in: query
description: Version ID of the object (if versioning is enabled)
schema:
type: string
maxLength: 64
example: "version_123"
ContentMD5:
name: Content-MD5
in: header
description: MD5 digest of the request body
schema:
type: string
pattern: '^[A-Za-z0-9+/]+=*$'
example: "n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg="
ContentType:
name: Content-Type
in: header
description: MIME type of the object
schema:
type: string
maxLength: 255
default: "application/octet-stream"
example: "image/jpeg"
ContentLength:
name: Content-Length
in: header
description: Size of the object in bytes
schema:
type: integer
minimum: 0
example: 1024000
Range:
name: Range
in: header
description: Downloads the specified range bytes of an object
schema:
type: string
pattern: '^bytes=\d+-\d*$'
example: "bytes=0-1023"
IfMatch:
name: If-Match
in: header
description: Return the object only if its ETag matches the specified value
schema:
type: string
example: "\"9bb58f26192e4ba00f01e2e7b136bbd8\""
IfNoneMatch:
name: If-None-Match
in: header
description: Return the object only if its ETag does not match the specified value
schema:
type: string
example: "\"9bb58f26192e4ba00f01e2e7b136bbd8\""
IfModifiedSince:
name: If-Modified-Since
in: header
description: Return the object only if it has been modified since the specified time
schema:
type: string
format: date-time
example: "Wed, 15 Jan 2023 10:30:00 GMT"
IfUnmodifiedSince:
name: If-Unmodified-Since
in: header
description: Return the object only if it has not been modified since the specified time
schema:
type: string
format: date-time
example: "Wed, 15 Jan 2023 10:30:00 GMT"
CacheControl:
name: Cache-Control
in: header
description: Specifies caching behavior for the object
schema:
type: string
example: "max-age=3600"
ContentDisposition:
name: Content-Disposition
in: header
description: Specifies presentational information for the object
schema:
type: string
example: "attachment; filename=\"file.txt\""
ContentEncoding:
name: Content-Encoding
in: header
description: Specifies what content encodings have been applied to the object
schema:
type: string
example: "gzip"
ContentLanguage:
name: Content-Language
in: header
description: Language the content is in
schema:
type: string
example: "en-US"
Expires:
name: Expires
in: header
description: Date and time when the object expires
schema:
type: string
format: date-time
example: "Wed, 15 Jan 2024 10:30:00 GMT"
ServerSideEncryption:
name: x-amz-server-side-encryption
in: header
description: Server-side encryption algorithm to use
schema:
type: string
enum: ["AES256", "aws:kms"]
example: "AES256"
StorageClass:
name: x-amz-storage-class
in: header
description: Storage class for the object
schema:
type: string
enum: ["STANDARD", "REDUCED_REDUNDANCY", "GLACIER", "DEEP_ARCHIVE"]
default: "STANDARD"
example: "STANDARD"
Tagging:
name: x-amz-tagging
in: header
description: Object tag set as a query string
schema:
type: string
example: "key1=value1&key2=value2"
MetadataDirective:
name: x-amz-metadata-directive
in: header
description: Specifies whether metadata is copied from the source object or replaced
schema:
type: string
enum: ["COPY", "REPLACE"]
default: "COPY"
example: "REPLACE"
CopySource:
name: x-amz-copy-source
in: header
description: Source bucket and object for copy operations
schema:
type: string
example: "/source-bucket/source-object"
ACL:
name: x-amz-acl
in: header
description: Canned ACL to apply to the object
schema:
type: string
enum: ["private", "public-read", "public-read-write", "authenticated-read"]
default: "private"
example: "public-read"

View File

@@ -0,0 +1,582 @@
# S3 API Endpoints
# List all buckets
/s3/:
get:
operationId: listBuckets
tags:
- S3 Buckets
summary: List buckets
description: Returns a list of all buckets owned by the authenticated sender of the request
security:
- AWS4-HMAC-SHA256: []
responses:
'200':
description: Success
content:
application/xml:
schema:
$ref: './schemas.yaml#/CreatePresignedUrlResponse'#/ListAllMyBucketsResult'
'401':
description: Unauthorized
# Bucket operations
/s3/{bucket}:
parameters:
- $ref: './parameters.yaml#/BucketName'
head:
operationId: headBucket
tags:
- S3 Buckets
summary: Check if bucket exists
description: Determines if a bucket exists and you have permission to access it
security:
- AWS4-HMAC-SHA256: []
responses:
'200':
description: Bucket exists and you have access
put:
operationId: createBucket
tags:
- S3 Buckets
summary: Create bucket
description: Creates a new S3 bucket
security:
- AWS4-HMAC-SHA256: []
responses:
'200':
description: Bucket created successfully
headers:
Location:
description: Location of the created bucket
schema:
type: string
'409':
description: Bucket already exists
delete:
operationId: deleteBucket
tags:
- S3 Buckets
summary: Delete bucket
description: Deletes the S3 bucket. The bucket must be empty before it can be deleted
security:
- AWS4-HMAC-SHA256: []
responses:
'204':
description: Bucket deleted successfully
'409':
description: Bucket not empty
get:
operationId: listObjects
tags:
- S3 Buckets
summary: List objects in bucket
description: Returns some or all of the objects in a bucket
security:
- AWS4-HMAC-SHA256: []
parameters:
- $ref: './parameters.yaml#/Prefix'
- $ref: './parameters.yaml#/Marker'
- $ref: './parameters.yaml#/MaxKeys'
- $ref: './parameters.yaml#/Delimiter'
- name: uploads
in: query
description: List multipart uploads when present
schema:
type: string
enum: ['']
responses:
'200':
description: Success
content:
application/xml:
schema:
oneOf:
- $ref: './schemas.yaml#/ListBucketResult'
- $ref: './schemas.yaml#/ListMultipartUploadsResult'
# Object operations
/s3/{bucket}/{key}:
parameters:
- $ref: './parameters.yaml#/BucketName'
- $ref: './parameters.yaml#/ObjectKey'
head:
operationId: headObject
tags:
- S3 Objects
summary: Get object metadata
description: Retrieves metadata from an object without returning the object itself
security:
- AWS4-HMAC-SHA256: []
responses:
'200':
description: Success
headers:
Content-Length:
schema:
type: integer
Content-Type:
schema:
type: string
ETag:
schema:
type: string
Last-Modified:
schema:
type: string
get:
operationId: getObject
tags:
- S3 Objects
summary: Get object
description: Retrieves objects from Amazon S3
security:
- AWS4-HMAC-SHA256: []
parameters:
- name: uploadId
in: query
description: List parts when present with upload ID
schema:
type: string
responses:
'200':
description: Success
content:
application/octet-stream:
schema:
type: string
format: binary
application/xml:
schema:
$ref: './schemas.yaml#/ListPartsResult'
'404':
description: Object not found
put:
operationId: putObject
tags:
- S3 Objects
summary: Put object
description: Adds an object to a bucket
security:
- AWS4-HMAC-SHA256: []
parameters:
- name: partNumber
in: query
description: Part number for multipart upload
schema:
type: integer
minimum: 1
maximum: 10000
- name: uploadId
in: query
description: Upload ID for multipart upload part
schema:
type: string
requestBody:
description: Object data
content:
application/octet-stream:
schema:
type: string
format: binary
responses:
'200':
description: Success
headers:
ETag:
schema:
type: string
delete:
operationId: deleteObject
tags:
- S3 Objects
summary: Delete object
description: Removes the null version of an object and inserts a delete marker
security:
- AWS4-HMAC-SHA256: []
parameters:
- name: uploadId
in: query
description: Abort multipart upload when present
schema:
type: string
responses:
'204':
description: Object deleted successfully
post:
operationId: multipartUpload
tags:
- S3 Multipart
summary: Initiate or complete multipart upload
description: Initiates a multipart upload or completes a multipart upload by assembling uploaded parts
security:
- AWS4-HMAC-SHA256: []
parameters:
- name: uploads
in: query
description: Initiate multipart upload when present
schema:
type: string
enum: ['']
- name: uploadId
in: query
description: Complete multipart upload with this upload ID
schema:
type: string
requestBody:
description: Complete multipart upload request
content:
application/xml:
schema:
$ref: './schemas.yaml#/CompleteMultipartUpload'
responses:
'200':
description: Success
content:
application/xml:
schema:
oneOf:
- $ref: './schemas.yaml#/InitiateMultipartUploadResult'
- $ref: './schemas.yaml#/CompleteMultipartUploadResult'
# Management API Endpoints
/api/stats:
get:
operationId: getSystemStats
tags:
- Management - Stats
summary: Get system statistics
description: Returns overall system statistics including storage usage, object counts, etc.
security:
- ApiKey: []
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: './schemas.yaml#/SystemStats'
/api/credentials:
get:
operationId: listCredentials
tags:
- Management - Credentials
summary: List credentials
description: Returns a list of all access credentials
security:
- ApiKey: []
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: './schemas.yaml#/CredentialsList'
post:
operationId: createCredential
tags:
- Management - Credentials
summary: Create credential
description: Creates a new access credential
security:
- ApiKey: []
requestBody:
required: true
content:
application/json:
schema:
$ref: './schemas.yaml#/CreateCredentialRequest'
responses:
'201':
description: Credential created
content:
application/json:
schema:
$ref: './schemas.yaml#/Credential'
/api/credentials/{id}:
parameters:
- $ref: './parameters.yaml#/CredentialId'
get:
operationId: getCredential
tags:
- Management - Credentials
summary: Get credential details
description: Returns detailed information about a specific credential
security:
- ApiKey: []
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: './schemas.yaml#/CredentialDetail'
put:
operationId: updateCredential
tags:
- Management - Credentials
summary: Update credential
description: Updates an existing credential
security:
- ApiKey: []
requestBody:
required: true
content:
application/json:
schema:
$ref: './schemas.yaml#/UpdateCredentialRequest'
responses:
'200':
description: Credential updated
content:
application/json:
schema:
$ref: './schemas.yaml#/ApiResponse'
delete:
operationId: deleteCredential
tags:
- Management - Credentials
summary: Delete credential
description: Deletes an access credential
security:
- ApiKey: []
responses:
'200':
description: Credential deleted
content:
application/json:
schema:
$ref: './schemas.yaml#/ApiResponse'
/api/buckets:
get:
operationId: listBucketsManagement
tags:
- Management - Buckets
summary: List all buckets
description: Returns a list of all buckets with detailed information
security:
- ApiKey: []
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: './schemas.yaml#/BucketsList'
post:
operationId: createBucketManagement
tags:
- Management - Buckets
summary: Create bucket
description: Creates a new bucket via management API
security:
- ApiKey: []
requestBody:
required: true
content:
application/json:
schema:
$ref: './schemas.yaml#/CreateBucketRequest'
responses:
'201':
description: Bucket created
content:
application/json:
schema:
$ref: './schemas.yaml#/BucketDetail'
/api/buckets/{bucketName}:
parameters:
- $ref: './parameters.yaml#/BucketName'
get:
operationId: getBucketDetails
tags:
- Management - Buckets
summary: Get bucket details
description: Returns detailed information about a specific bucket including objects
security:
- ApiKey: []
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: './schemas.yaml#/BucketDetail'
delete:
operationId: deleteBucketManagement
tags:
- Management - Buckets
summary: Delete bucket
description: Deletes a bucket via management API
security:
- ApiKey: []
responses:
'200':
description: Bucket deleted
content:
application/json:
schema:
$ref: './schemas.yaml#/ApiResponse'
/api/buckets/{bucketName}/objects:
parameters:
- $ref: './parameters.yaml#/BucketName'
get:
operationId: listObjectsManagement
tags:
- Management - Objects
summary: List objects in bucket
description: Returns a list of objects in the specified bucket
security:
- ApiKey: []
parameters:
- $ref: './parameters.yaml#/Prefix'
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: './schemas.yaml#/ObjectsList'
delete:
operationId: deleteMultipleObjects
tags:
- Management - Objects
summary: Delete multiple objects
description: Deletes multiple objects from a bucket
security:
- ApiKey: []
requestBody:
required: true
content:
application/json:
schema:
$ref: './schemas.yaml#/DeleteObjectsRequest'
responses:
'200':
description: Objects deleted
content:
application/json:
schema:
$ref: './schemas.yaml#/DeleteObjectsResponse'
/api/buckets/{bucketName}/objects/{objectKey}:
parameters:
- $ref: './parameters.yaml#/BucketName'
- $ref: './parameters.yaml#/ObjectKey'
get:
operationId: getObjectDetails
tags:
- Management - Objects
summary: Get object details
description: Returns detailed information about a specific object
security:
- ApiKey: []
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: './schemas.yaml#/ObjectDetail'
delete:
operationId: deleteObjectManagement
tags:
- Management - Objects
summary: Delete object
description: Deletes a specific object
security:
- ApiKey: []
responses:
'200':
description: Object deleted
content:
application/json:
schema:
$ref: './schemas.yaml#/ApiResponse'
/api/buckets/{bucketName}/multipart-uploads:
parameters:
- $ref: './parameters.yaml#/BucketName'
get:
operationId: listMultipartUploads
tags:
- Management - Objects
summary: List multipart uploads
description: Returns active multipart uploads for a bucket
security:
- ApiKey: []
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: './schemas.yaml#/MultipartUploadsList'
/api/presigned-urls:
get:
operationId: listPresignedUrls
tags:
- Management - Presigned URLs
summary: List presigned URLs
description: Returns active presigned URLs
security:
- ApiKey: []
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: './schemas.yaml#/PresignedUrlsList'
post:
operationId: createPresignedUrl
tags:
- Management - Presigned URLs
summary: Create presigned URL
description: Generates a new presigned URL
security:
- ApiKey: []
requestBody:
required: true
content:
application/json:
schema:
$ref: './schemas.yaml#/CreatePresignedUrlRequest'
responses:
'201':
description: Presigned URL created
content:
application/json:
schema:
$ref: './schemas.yaml#/CreatePresignedUrlResponse'

View File

@@ -0,0 +1,633 @@
# S3 API Schemas
S3Error:
type: object
xml:
name: Error
properties:
Code:
type: string
example: "NoSuchBucket"
Message:
type: string
example: "The specified bucket does not exist"
Resource:
type: string
example: "/mybucket"
RequestId:
type: string
example: "4442587FB7D0A2F9"
ListAllMyBucketsResult:
type: object
xml:
name: ListAllMyBucketsResult
namespace: http://s3.amazonaws.com/doc/2006-03-01/
properties:
Buckets:
type: object
properties:
Bucket:
type: array
items:
$ref: '#/Bucket'
Bucket:
type: object
properties:
Name:
type: string
example: "my-bucket"
CreationDate:
type: string
format: date-time
example: "2023-01-15T10:30:00.000Z"
ListBucketResult:
type: object
xml:
name: ListBucketResult
namespace: http://s3.amazonaws.com/doc/2006-03-01/
properties:
Name:
type: string
example: "my-bucket"
Prefix:
type: string
example: "photos/"
Marker:
type: string
example: "photos/2023/"
MaxKeys:
type: integer
example: 1000
IsTruncated:
type: boolean
example: false
Contents:
type: array
items:
$ref: '#/ObjectSummary'
ObjectSummary:
type: object
properties:
Key:
type: string
example: "photos/2023/vacation.jpg"
LastModified:
type: string
format: date-time
example: "2023-01-15T10:30:00.000Z"
ETag:
type: string
example: "\"9bb58f26192e4ba00f01e2e7b136bbd8\""
Size:
type: integer
example: 1024000
StorageClass:
type: string
example: "STANDARD"
InitiateMultipartUploadResult:
type: object
xml:
name: InitiateMultipartUploadResult
namespace: http://s3.amazonaws.com/doc/2006-03-01/
properties:
Bucket:
type: string
example: "my-bucket"
Key:
type: string
example: "large-file.zip"
UploadId:
type: string
example: "upload_12345_abcdef"
CompleteMultipartUpload:
type: object
xml:
name: CompleteMultipartUpload
properties:
Part:
type: array
items:
type: object
properties:
PartNumber:
type: integer
example: 1
ETag:
type: string
example: "\"9bb58f26192e4ba00f01e2e7b136bbd8\""
CompleteMultipartUploadResult:
type: object
xml:
name: CompleteMultipartUploadResult
namespace: http://s3.amazonaws.com/doc/2006-03-01/
properties:
Location:
type: string
example: "/my-bucket/large-file.zip"
Bucket:
type: string
example: "my-bucket"
Key:
type: string
example: "large-file.zip"
ETag:
type: string
example: "\"9bb58f26192e4ba00f01e2e7b136bbd8-5\""
ListPartsResult:
type: object
xml:
name: ListPartsResult
namespace: http://s3.amazonaws.com/doc/2006-03-01/
properties:
Bucket:
type: string
example: "my-bucket"
Key:
type: string
example: "large-file.zip"
UploadId:
type: string
example: "upload_12345_abcdef"
StorageClass:
type: string
example: "STANDARD"
PartNumberMarker:
type: integer
example: 0
NextPartNumberMarker:
type: integer
example: 5
MaxParts:
type: integer
example: 1000
IsTruncated:
type: boolean
example: false
Part:
type: array
items:
$ref: '#/Part'
Part:
type: object
properties:
PartNumber:
type: integer
example: 1
LastModified:
type: string
format: date-time
example: "2023-01-15T10:30:00.000Z"
ETag:
type: string
example: "\"9bb58f26192e4ba00f01e2e7b136bbd8\""
Size:
type: integer
example: 5242880
ListMultipartUploadsResult:
type: object
xml:
name: ListMultipartUploadsResult
namespace: http://s3.amazonaws.com/doc/2006-03-01/
properties:
Bucket:
type: string
example: "my-bucket"
KeyMarker:
type: string
example: ""
UploadIdMarker:
type: string
example: ""
NextKeyMarker:
type: string
example: ""
NextUploadIdMarker:
type: string
example: ""
MaxUploads:
type: integer
example: 1000
IsTruncated:
type: boolean
example: false
Upload:
type: array
MultipartUpload:
type: object
properties:
Key:
type: string
example: "large-file.zip"
UploadId:
type: string
example: "upload_12345_abcdef"
Initiated:
type: string
format: date-time
example: "2023-01-15T10:30:00.000Z"
StorageClass:
type: string
example: "STANDARD"
# Management API Schemas
ApiError:
type: object
properties:
error:
type: string
example: "Resource not found"
code:
type: string
example: "NOT_FOUND"
message:
type: string
example: "The requested resource could not be found"
ApiResponse:
type: object
properties:
message:
type: string
example: "Operation completed successfully"
SystemStats:
type: object
properties:
credentials:
type: integer
example: 5
description: Total number of access credentials
buckets:
type: integer
example: 12
description: Total number of buckets
objects:
type: integer
example: 1250
description: Total number of objects
total_storage:
type: integer
example: 1073741824
description: Total storage used in bytes
total_storage_human:
type: string
example: "1.0 GB"
description: Human-readable storage size
active_multipart_uploads:
type: integer
example: 3
description: Number of active multipart uploads
active_presigned_urls:
type: integer
example: 15
description: Number of active presigned URLs
Credential:
type: object
properties:
id:
type: integer
example: 1
access_key:
type: string
example: "AKIAIOSFODNN7EXAMPLE"
secret_key:
type: string
example: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
description: Only returned when creating credential
user_name:
type: string
example: "john-doe"
nullable: true
is_active:
type: boolean
example: true
created_at:
type: string
format: date-time
example: "2023-01-15T10:30:00Z"
CredentialSummary:
type: object
properties:
id:
type: integer
example: 1
access_key:
type: string
example: "AKIAIOSFODNN7EXAMPLE"
user_name:
type: string
example: "john-doe"
nullable: true
is_active:
type: boolean
example: true
created_at:
type: string
format: date-time
example: "2023-01-15T10:30:00Z"
bucket_count:
type: integer
example: 3
CredentialDetail:
allOf:
- $ref: '#/CredentialSummary'
- type: object
properties:
buckets:
type: array
items:
$ref: '#/BucketSummary'
CredentialsList:
type: object
properties:
credentials:
type: array
items:
$ref: '#/CredentialSummary'
CreateCredentialRequest:
type: object
properties:
user_name:
type: string
example: "john-doe"
description: Optional user name
access_key:
type: string
example: "AKIAIOSFODNN7EXAMPLE"
description: Optional custom access key (auto-generated if not provided)
secret_key:
type: string
example: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
description: Optional custom secret key (auto-generated if not provided)
UpdateCredentialRequest:
type: object
properties:
user_name:
type: string
example: "john-doe"
is_active:
type: boolean
example: true
BucketSummary:
type: object
properties:
name:
type: string
example: "my-bucket"
region:
type: string
example: "us-east-1"
owner:
type: string
example: "john-doe"
created_at:
type: string
format: date-time
example: "2023-01-15T10:30:00Z"
object_count:
type: integer
example: 150
total_size:
type: integer
example: 1073741824
total_size_human:
type: string
example: "1.0 GB"
BucketDetail:
allOf:
- $ref: '#/BucketSummary'
- type: object
properties:
objects:
type: array
items:
$ref: '#/ObjectInfo'
BucketsList:
type: object
properties:
buckets:
type: array
items:
$ref: '#/BucketSummary'
CreateBucketRequest:
type: object
required:
- name
- owner_id
properties:
name:
type: string
example: "my-new-bucket"
pattern: '^[a-z0-9\-\.]+$'
owner_id:
type: integer
example: 1
region:
type: string
example: "us-east-1"
default: "us-east-1"
ObjectInfo:
type: object
properties:
key:
type: string
example: "photos/vacation.jpg"
size:
type: integer
example: 1024000
size_human:
type: string
example: "1.0 MB"
content_type:
type: string
example: "image/jpeg"
etag:
type: string
example: "9bb58f26192e4ba00f01e2e7b136bbd8"
is_multipart:
type: boolean
example: false
created_at:
type: string
format: date-time
example: "2023-01-15T10:30:00Z"
ObjectDetail:
allOf:
- $ref: '#/ObjectInfo'
- type: object
properties:
part_count:
type: integer
example: 0
metadata:
type: object
additionalProperties:
type: string
example:
user-id: "12345"
category: "photos"
storage_path:
type: string
example: "my-bucket/objects/ab/cd/abcdef123456..."
updated_at:
type: string
format: date-time
example: "2023-01-15T10:30:00Z"
ObjectsList:
type: object
properties:
objects:
type: array
items:
$ref: '#/ObjectInfo'
DeleteObjectsRequest:
type: object
required:
- keys
properties:
keys:
type: array
items:
type: string
example: ["file1.txt", "file2.txt", "folder/file3.txt"]
DeleteObjectsResponse:
type: object
properties:
deleted:
type: array
items:
type: string
example: ["file1.txt", "file2.txt"]
MultipartUploadInfo:
type: object
properties:
upload_id:
type: string
example: "upload_12345_abcdef"
object_key:
type: string
example: "large-file.zip"
initiated_by:
type: string
example: "john-doe"
content_type:
type: string
example: "application/zip"
initiated_at:
type: string
format: date-time
example: "2023-01-15T10:30:00Z"
part_count:
type: integer
example: 5
total_size:
type: integer
example: 26214400
total_size_human:
type: string
example: "25.0 MB"
MultipartUploadsList:
type: object
properties:
uploads:
type: array
items:
$ref: '#/MultipartUploadInfo'
PresignedUrlInfo:
type: object
properties:
bucket_name:
type: string
example: "my-bucket"
object_key:
type: string
example: "file.txt"
method:
type: string
enum: [GET, PUT, POST, DELETE]
example: "GET"
access_key:
type: string
example: "AKIAIOSFODNN7EXAMPLE"
expires_at:
type: string
format: date-time
example: "2023-01-15T11:30:00Z"
created_at:
type: string
format: date-time
example: "2023-01-15T10:30:00Z"
PresignedUrlsList:
type: object
properties:
urls:
type: array
items:
$ref: '#/PresignedUrlInfo'
CreatePresignedUrlRequest:
type: object
required:
- bucket_name
- object_key
- access_key
properties:
bucket_name:
type: string
example: "my-bucket"
object_key:
type: string
example: "file.txt"
method:
type: string
enum: [GET, PUT, POST, DELETE]
default: "GET"
example: "GET"
expires_in:
type: integer
minimum: 1
maximum: 604800
default: 3600
example: 3600
description: Expiration time in seconds (max 7 days)
access_key:
type: string
example: "AKIAIOSFODNN7EXAMPLE"
CreatePresignedUrlResponse:
type: object
properties:
url:
type: string
example: "/my-bucket/file.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20230115%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20230115T103000Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=abc123&hash=def456"

View File

@@ -0,0 +1,101 @@
AWS4-HMAC-SHA256:
type: apiKey
description: |
AWS Signature Version 4 authentication for S3 API endpoints.
## Authentication Process
1. Create a canonical request
2. Create a string to sign
3. Calculate the signature using HMAC-SHA256
4. Add the authorization header or query parameters
## Authorization Header Format
```
Authorization: AWS4-HMAC-SHA256 Credential=<AccessKey>/<Date>/<Region>/s3/aws4_request,SignedHeaders=<SignedHeaders>,Signature=<Signature>
```
## Query String Format (for presigned URLs)
```
?X-Amz-Algorithm=AWS4-HMAC-SHA256
&X-Amz-Credential=<AccessKey>/<Date>/<Region>/s3/aws4_request
&X-Amz-Date=<Timestamp>
&X-Amz-Expires=<ExpirationTime>
&X-Amz-SignedHeaders=<SignedHeaders>
&X-Amz-Signature=<Signature>
```
## Required Headers
- `Authorization`: The calculated authorization value
- `x-amz-date`: Timestamp in ISO 8601 format (YYYYMMDDTHHMMSSZ)
- `x-amz-content-sha256`: SHA256 hash of the request payload
## Example
```
Authorization: AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20230115/us-east-1/s3/aws4_request,SignedHeaders=host;x-amz-date,Signature=fe5f80f77d5fa3beca038a248ff027d0445342fe2855ddc963176630326f1024
x-amz-date: 20230115T103000Z
x-amz-content-sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
```
name: Authorization
in: header
ApiKey:
type: apiKey
description: |
API key authentication for management console endpoints.
## Usage
Include your API key in the `X-API-Key` header for all management API requests.
## Example
```
X-API-Key: your-api-key-here
```
## Obtaining an API Key
API keys can be generated through the management console or by contacting your administrator.
## Permissions
API keys have full access to the management API and should be kept secure.
name: X-API-Key
in: header
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: |
JWT Bearer token authentication (optional alternative for management API).
## Usage
Include the JWT token in the Authorization header:
```
Authorization: Bearer <jwt-token>
```
## Token Structure
The JWT token contains claims about the user's permissions and expires after a set period.
## Example
```
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```
BasicAuth:
type: http
scheme: basic
description: |
HTTP Basic authentication (for simple setups).
## Usage
Encode username:password in Base64 and include in Authorization header:
```
Authorization: Basic <base64-encoded-credentials>
```
## Example
```
Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=
```
## Note
Basic auth should only be used over HTTPS in production environments.

117
templates/openapi/v1.yaml Normal file
View File

@@ -0,0 +1,117 @@
openapi: "3.0.0"
info:
version: "1.0.0"
title: vStash S3 Compatible API
description: |
A high-performance S3-compatible object storage API with management console.
## Features
- Full S3 API compatibility for basic operations
- Multipart upload support
- Presigned URLs
- Management console API
- Enterprise storage backend
## Authentication
Uses AWS Signature Version 4 for S3 API endpoints and API keys for management endpoints.
contact:
email: support@vultr.com
name: 'vStash Support'
url: https://www.vultr.com
x-logo:
url: 'https://www.vultr.com/dist/img/brand/vultr-logo-onwhite.svg'
backgroundColor: '#fafafa'
altText: 'vStash - S3 Compatible API and Management'
servers:
- url: https://vstash.vultr.com
description: Production server
- url: https://vstash-staging.vultr.com
description: Staging server
- url: http://localhost:8000
description: Development server
tags:
- name: S3 Buckets
description: S3 bucket operations
- name: S3 Objects
description: S3 object operations
- name: S3 Multipart
description: S3 multipart upload operations
- name: Management - Stats
description: System statistics and monitoring
- name: Management - Credentials
description: Access key and credential management
- name: Management - Buckets
description: Bucket management via console
- name: Management - Objects
description: Object management via console
- name: Management - Presigned URLs
description: Presigned URL management
paths:
$ref: './paths.yaml'
components:
schemas:
$ref: './schemas.yaml'
parameters:
$ref: './parameters.yaml'
securitySchemes:
$ref: './security.yaml'
responses:
'400':
description: Bad Request
content:
application/xml:
schema:
$ref: '#/components/schemas/S3Error'
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'401':
description: Unauthorized
content:
application/xml:
schema:
$ref: '#/components/schemas/S3Error'
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'403':
description: Forbidden
content:
application/xml:
schema:
$ref: '#/components/schemas/S3Error'
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'404':
description: Not Found
content:
application/xml:
schema:
$ref: '#/components/schemas/S3Error'
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'409':
description: Conflict
content:
application/xml:
schema:
$ref: '#/components/schemas/S3Error'
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'500':
description: Internal Server Error
content:
application/xml:
schema:
$ref: '#/components/schemas/S3Error'
application/json:
schema:
$ref: '#/components/schemas/ApiError'

View File

@@ -0,0 +1,20 @@
#!/bin/bash
cd templates/openapi
echo "Checking YAML syntax..."
echo "Checking v1.yaml..."
python3 -c "import yaml; yaml.safe_load(open('v1.yaml'))" && echo "✅ v1.yaml is valid" || echo "❌ v1.yaml has syntax errors"
echo "Checking paths.yaml..."
python3 -c "import yaml; yaml.safe_load(open('paths.yaml'))" && echo "✅ paths.yaml is valid" || echo "❌ paths.yaml has syntax errors"
echo "Checking schemas.yaml..."
python3 -c "import yaml; yaml.safe_load(open('schemas.yaml'))" && echo "✅ schemas.yaml is valid" || echo "❌ schemas.yaml has syntax errors"
echo "Checking parameters.yaml..."
python3 -c "import yaml; yaml.safe_load(open('parameters.yaml'))" && echo "✅ parameters.yaml is valid" || echo "❌ parameters.yaml has syntax errors"
echo "Checking security.yaml..."
python3 -c "import yaml; yaml.safe_load(open('security.yaml'))" && echo "✅ security.yaml is valid" || echo "❌ security.yaml has syntax errors"