diff --git a/.env.exampl b/.env.exampl
new file mode 100644
index 0000000..3d37e9d
--- /dev/null
+++ b/.env.exampl
@@ -0,0 +1,9 @@
+# Database Configuration
+DATABASE_URL=postgresql://username:password@localhost:5432/filestore
+
+# Upload Directory
+UPLOAD_DIR=./uploads
+
+# Server Configuration
+HOST=0.0.0.0
+PORT=8000
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..dc3ecbd
--- /dev/null
+++ b/README.md
@@ -0,0 +1,367 @@
+# Das File Storage Web Application
+
+A production-ready file storage service built with FastAPI, PostgreSQL, and session-based authentication.
+
+## Features
+
+- **Web Interface**: Modern, responsive GUI for users and admins
+- **User Authentication**: Session-based auth for web, HTTP Basic Auth and API Key for REST API
+- **Role-Based Access**: Admin and regular user roles
+- **File Management**: Upload, download, delete files via web UI or API
+- **Secure Sharing**: Generate secure share links without exposing usernames or filenames
+- **Admin Panel**: Full user management interface (create, delete, block users)
+- **Settings Page**: Change password, manage API keys
+- **Password Security**: PBKDF2-SHA256 password hashing
+- **Database**: PostgreSQL for all metadata storage
+
+## Quick Start
+
+### Using Docker Compose (Recommended)
+
+1. Clone the repository
+2. Update database credentials in `docker-compose.yml`
+3. Run:
+```bash
+docker-compose up -d
+```
+
+The application will be available at `http://localhost:8000` by default
+
+### Project Structure
+
+```
+project/
+├── main.py # Main application file
+├── requirements.txt # Python dependencies
+├── docker-compose.yml # Docker configuration
+├── Dockerfile # Container image
+├── .env # Environment variables
+├── templates/ # HTML templates
+│ ├── base.html # Base template
+│ ├── login.html # Login page
+│ ├── user_files.html # User dashboard
+│ ├── settings.html # Settings page
+│ └── admin.html # Admin panel
+└── uploads/ # File storage directory
+```
+
+### Manual Installation
+
+1. Install PostgreSQL and create a database:
+```sql
+CREATE DATABASE filestore;
+```
+
+2. Install Python dependencies:
+```bash
+pip install -r requirements.txt
+```
+
+3. Create `.env` file:
+```bash
+cp .env.example .env
+# Edit .env with your database credentials
+```
+
+4. Update `configs/server_config.yaml` file with your own server's configuration (including `secrets.yaml` if needed)
+
+5. Create templates directory and add HTML files:
+```bash
+mkdir templates
+# Copy all template files (base.html, login.html, user_files.html, settings.html, admin.html)
+```
+
+6. Run the application:
+```bash
+python main.py
+```
+
+Or with uvicorn:
+```bash
+uvicorn main:app --host 0.0.0.0 --port 8000 --reload
+```
+
+## Default Admin Account
+
+On first startup, a default admin account is created:
+- **Username**: `admin`
+- **Password**: `admin123`
+- **⚠️ Change this password immediately in production!**
+
+The admin API key will be printed in the console on first startup.
+
+## Web Interface
+
+### User Interface
+
+1. **Login Page** (`/login`)
+ - Clean, modern login interface
+ - Username and password authentication
+
+2. **Dashboard** (`/dashboard`)
+ - View all your uploaded files
+ - Upload new files with drag-and-drop
+ - Download files
+ - Generate and copy share links
+ - Delete files
+ - File size and upload date information
+
+3. **Settings** (`/settings`)
+ - Change your password
+ - View and copy your API key
+ - Regenerate API key
+ - View account information
+
+### Admin Interface
+
+4. **Admin Panel** (`/admin`)
+ - Dashboard statistics (total users, active/blocked, total files)
+ - User management table
+ - Create new users
+ - Block/unblock users
+ - Delete users
+ - View file counts per user
+
+### Navigation
+
+All pages have a navigation bar with:
+ - User badge showing username and role
+ - Quick access to dashboard, settings, and admin panel (if admin)
+ - Logout button
+
+## API Documentation
+
+Once running, visit:
+- Interactive API docs: `http://localhost:8000/docs`
+- Alternative docs: `http://localhost:8000/redoc`
+
+## API Endpoints
+
+### Authentication
+
+**HTTP Basic Auth**: Use username and password in the Authorization header
+```bash
+curl -u username:password http://localhost:8000/api/files
+```
+
+**API Key Auth**: Use X-API-Key header
+```bash
+curl -H "X-API-Key: your-api-key" http://localhost:8000/api/files
+```
+
+### User Management (Admin Only)
+
+#### Create User
+```bash
+POST /api/users
+Authorization: Basic admin:admin123
+Content-Type: application/json
+
+{
+ "username": "newuser",
+ "password": "securepass123",
+ "is_admin": false
+}
+```
+
+#### List Users
+```bash
+GET /api/users
+Authorization: Basic admin:admin123
+```
+
+#### Delete User
+```bash
+DELETE /api/users/{user_id}
+Authorization: Basic admin:admin123
+```
+
+#### Block/Unblock User
+```bash
+PATCH /api/users/{user_id}/block?block=true
+Authorization: Basic admin:admin123
+```
+
+#### Get Your API Key
+```bash
+GET /api/users/me/api-key
+Authorization: Basic username:password
+```
+
+#### Regenerate API Key
+```bash
+POST /api/users/me/api-key/regenerate
+Authorization: Basic username:password
+```
+
+### File Management
+
+#### Upload File
+```bash
+POST /api/files/upload
+Authorization: Basic username:password
+Content-Type: multipart/form-data
+
+file=@/path/to/file.pdf
+```
+
+With API Key:
+```bash
+curl -X POST \
+ -H "X-API-Key: your-api-key" \
+ -F "file=@document.pdf" \
+ http://localhost:8000/api/files/upload
+```
+
+#### List Your Files
+```bash
+GET /api/files
+Authorization: Basic username:password
+```
+
+#### Download File
+```bash
+GET /api/files/{file_id}/download
+Authorization: Basic username:password
+```
+
+#### Delete File
+```bash
+DELETE /api/files/{file_id}
+Authorization: Basic username:password
+```
+
+### Public File Sharing
+
+#### Download Shared File (No Auth Required)
+```bash
+GET /share/{share_token}
+```
+
+Example: `http://localhost:8000/share/abc123xyz...`
+
+## Database Schema
+
+### Users Table
+- `id`: Primary key
+- `username`: Unique username
+- `password_hash`: PBKDF2-SHA256 hashed password
+- `is_admin`: Admin role flag
+- `is_blocked`: Block status
+- `api_key`: Unique API key for REST authentication
+- `created_at`: Account creation timestamp
+
+### Files Table
+- `id`: Primary key
+- `filename`: Original filename
+- `stored_filename`: Unique stored filename
+- `file_size`: File size in bytes
+- `content_type`: MIME type
+- `share_token`: Secure token for public sharing
+- `user_id`: Foreign key to users
+- `uploaded_at`: Upload timestamp
+
+## Security Features
+
+1. **Password Hashing**: PBKDF2-SHA256 with 100,000 iterations
+2. **Secure Tokens**: Cryptographically secure random tokens for API keys and share links
+3. **User Isolation**: Users can only access their own files
+4. **Admin Controls**: Block/unblock users, manage accounts
+5. **Share Links**: No exposure of usernames or original filenames
+
+## Production Deployment
+
+### Environment Variables
+
+```bash
+DATABASE_URL=postgresql://user:pass@host:port/dbname
+UPLOAD_DIR=/var/app/uploads
+```
+
+### Security Checklist
+
+- [ ] Change default admin password
+- [ ] Use strong database credentials
+- [ ] Enable HTTPS/TLS
+- [ ] Set up firewall rules
+- [ ] Configure backup strategy
+- [ ] Set up monitoring and logging
+- [ ] Limit file upload sizes
+- [ ] Implement rate limiting
+- [ ] Regular security updates
+
+### Nginx Configuration Example
+
+```nginx
+server {
+ listen 80;
+ server_name yourdomain.com;
+
+ client_max_body_size 100M;
+
+ location / {
+ proxy_pass http://localhost:8000;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+}
+```
+
+### PostgreSQL Optimization
+
+```sql
+-- Create indexes for better performance
+CREATE INDEX idx_files_user_id ON files(user_id);
+CREATE INDEX idx_files_share_token ON files(share_token);
+CREATE INDEX idx_users_username ON users(username);
+CREATE INDEX idx_users_api_key ON users(api_key);
+```
+
+## Testing
+
+### Create a Test User
+```bash
+curl -X POST http://localhost:8000/api/users \
+ -u admin:admin123 \
+ -H "Content-Type: application/json" \
+ -d '{"username": "testuser", "password": "testpass123"}'
+```
+
+### Upload a Test File
+```bash
+echo "Hello World" > test.txt
+curl -X POST http://localhost:8000/api/files/upload \
+ -u testuser:testpass123 \
+ -F "file=@test.txt"
+```
+
+### List Files
+```bash
+curl http://localhost:8000/api/files \
+ -u testuser:testpass123
+```
+
+## Troubleshooting
+
+### Database Connection Issues
+- Verify PostgreSQL is running
+- Check DATABASE_URL is correct
+- Ensure database exists and user has permissions
+
+### File Upload Issues
+- Check UPLOAD_DIR exists and has write permissions
+- Verify disk space is available
+- Check file size limits
+
+### Authentication Issues
+- Verify username/password are correct
+- Check if user is blocked
+- Ensure API key is valid and not regenerated
+
+## Support
+
+For issues and questions, please open an issue on the repository.
+
+###### _Made by -=:dAs:=-_
\ No newline at end of file
diff --git a/app_config.py b/app_config.py
index c5e32c5..c676091 100644
--- a/app_config.py
+++ b/app_config.py
@@ -1,30 +1,17 @@
import os
from config_file import read_config as read_cfg
-APP_VERSION="1.0"
+APP_VERSION="1.0.0"
SCRIPT_PATH = os.path.dirname(__file__)
CONFIGS_DIR = SCRIPT_PATH + "/configs"
CONFIG_FILE_NAME = CONFIGS_DIR + "/server_config.yaml"
API_KEY = ''
-UPLOAD_DIR = 'uploads'
+UPLOAD_DIR = '.uploads'
DB_URL = ''
APP_PORT = 25100
IS_DEBUG = False
IS_DOCS_AVAILABLE = False
-HTML_FILE = 'html/main.html'
-CSS_FILE = 'css/main.css'
-JS_FILE = 'js/main.js'
-ALIAS_NUM_LETTERS = 3
-ALIAS_NUM_DIGITS = 4
-"""
-For 3 + 4 defaults there are 1 406 080 000 possible values:
-Кількість варіантів для кожної літери (великої або малої) дорівнює 52 (26 великих + 26 малих).
-Кількість комбінацій для трьох літер: 52 * 52 * 52 = 140 608.
-Кількість варіантів для кожної цифри: 10 (0-9).
-Кількість комбінацій для чотирьох цифр: 10 * 10 * 10 * 10 = 10 000.
-Загальна кількість комбінацій: 140 608 * 10 000 = 1 406 080 000.
-"""
def read_app_config():
j, _ = read_cfg(CONFIG_FILE_NAME)
@@ -40,18 +27,12 @@ def get_config_value(cfg, section, key, default):
return cfg[section][key] if section in cfg and key in cfg[section] else default
def parse_config(cfg):
- global UPLOAD_DIR, APP_PORT, IS_DEBUG, DB_URL, ALIAS_NUM_LETTERS, ALIAS_NUM_DIGITS\
- , IS_DOCS_AVAILABLE, HTML_FILE, CSS_FILE, JS_FILE
+ global UPLOAD_DIR, APP_PORT, IS_DEBUG, DB_URL, IS_DOCS_AVAILABLE
UPLOAD_DIR = get_config_value(cfg, 'server', 'directory', UPLOAD_DIR)
APP_PORT = get_config_value(cfg, 'server', 'port', APP_PORT)
DB_URL = get_config_value(cfg, 'server', 'db_url', DB_URL)
IS_DEBUG = get_config_value(cfg, 'server', 'debug', IS_DEBUG)
- ALIAS_NUM_LETTERS = get_config_value(cfg, 'server', 'alias_letters', ALIAS_NUM_LETTERS)
- ALIAS_NUM_DIGITS = get_config_value(cfg, 'server', 'alias_digits', ALIAS_NUM_DIGITS)
IS_DOCS_AVAILABLE = get_config_value(cfg, 'server', 'docs_available', IS_DOCS_AVAILABLE)
- HTML_FILE = get_config_value(cfg, 'interface', 'html', HTML_FILE)
- CSS_FILE = get_config_value(cfg, 'interface', 'css', CSS_FILE)
- JS_FILE = get_config_value(cfg, 'interface', 'js', JS_FILE)
def init_config():
config = read_app_config()
diff --git a/configs/server_config.yaml b/configs/server_config.yaml
index dadfa93..9ecb40b 100644
--- a/configs/server_config.yaml
+++ b/configs/server_config.yaml
@@ -1,13 +1,8 @@
server:
port: !secret port
- directory: 'uploads'
+ directory: '.uploads'
db_url: !secret db_url
alias_letters: 3
alias_digits: 4
debug: false
- docs_available: false
-
-interface:
- html: 'html/main.html'
- css: 'css/main.css'
- script: 'js/main.js'
+ docs_available: true
diff --git a/css/main.css b/css/main.css
deleted file mode 100644
index 17c6f7b..0000000
--- a/css/main.css
+++ /dev/null
@@ -1,64 +0,0 @@
-body {
- font-family: Arial, sans-serif;
- max-width: 800px;
- margin: 50px auto;
- padding: 20px;
-}
-h1 { color: #333; }
-.section {
- background: #f5f5f5;
- padding: 20px;
- margin: 20px 0;
- border-radius: 8px;
-}
-input[type="file"], input[type="text"] {
- margin: 10px 0;
- padding: 8px;
- width: 100%;
- max-width: 400px;
-}
-button {
- background: #007bff;
- color: white;
- padding: 10px 20px;
- border: none;
- border-radius: 4px;
- cursor: pointer;
- margin: 5px;
-}
-button:hover {
- background: #0056b3;
-}
-.file-list {
- list-style: none;
- padding: 0;
-}
-.file-item {
- background: white;
- padding: 10px;
- margin: 5px 0;
- border-radius: 4px;
- display: flex;
- justify-content: space-between;
- align-items: center;
-}
-.message {
- padding: 10px;
- margin: 10px 0;
- border-radius: 4px;
-}
-.success { background: #d4edda; color: #155724; }
-.error { background: #f8d7da; color: #721c24; }
-.info { background: #d1ecf1; color: #0c5460; }
-.hidden { display: none; }
-.api-key-display {
- background: white;
- padding: 10px;
- border-radius: 4px;
- font-family: monospace;
- word-break: break-all;
-}
-.file-date {
- font-size: 0.85em;
- color: #676;
-}
\ No newline at end of file
diff --git a/docker-compose.yaml b/docker-compose.yaml
new file mode 100644
index 0000000..336feff
--- /dev/null
+++ b/docker-compose.yaml
@@ -0,0 +1,38 @@
+version: '3.8'
+
+services:
+ postgres:
+ image: postgres:15-alpine
+ container_name: filestore_db
+ environment:
+ POSTGRES_USER: filestore_user
+ POSTGRES_PASSWORD: secure_password_here
+ POSTGRES_DB: filestore
+ volumes:
+ - postgres_data:/var/lib/postgresql/data
+ ports:
+ - "5432:5432"
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U filestore_user"]
+ interval: 10s
+ timeout: 5s
+ retries: 5
+
+ app:
+ build: .
+ container_name: filestore_app
+ environment:
+ DATABASE_URL: postgresql://filestore_user:secure_password_here@postgres:5432/filestore
+ UPLOAD_DIR: /app/uploads
+ volumes:
+ - ./uploads:/app/uploads
+ - ./app:/app
+ ports:
+ - "8000:8000"
+ depends_on:
+ postgres:
+ condition: service_healthy
+ command: uvicorn main:app --host 0.0.0.0 --port 8000 --reload
+
+volumes:
+ postgres_data:
\ No newline at end of file
diff --git a/dockerfile b/dockerfile
new file mode 100644
index 0000000..8aea394
--- /dev/null
+++ b/dockerfile
@@ -0,0 +1,24 @@
+FROM python:3.13-slim
+
+WORKDIR /app
+
+# Install system dependencies
+RUN apt-get update && apt-get install -y \
+ postgresql-client \
+ && rm -rf /var/lib/apt/lists/*
+
+# Copy requirements and install Python dependencies
+COPY requirements.txt .
+RUN pip install --no-cache-dir -r requirements.txt
+
+# Copy application code
+COPY main.py .
+
+# Create uploads directory
+RUN mkdir -p /app/uploads
+
+# Expose port
+EXPOSE 8000
+
+# Run the application
+CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
\ No newline at end of file
diff --git a/html/main.html b/html/main.html
deleted file mode 100644
index 930b96a..0000000
--- a/html/main.html
+++ /dev/null
@@ -1,51 +0,0 @@
-
-
-
- Das File Server
-
-
-
-
- 🔐 Secure File Server by -=:dAs:=-
-
-
-
API Key Management
-
-
-
-
-
-
-
-
Your API Key:
-
-
⚠️ Save this key securely!
-
-
-
-
-
-
-
-
Upload Files
-
-
-
-
-
-
-
Your Files
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/js/main.js b/js/main.js
deleted file mode 100644
index 5bb6918..0000000
--- a/js/main.js
+++ /dev/null
@@ -1,171 +0,0 @@
-let apiKey = localStorage.getItem('apiKey');
-
-function updateUI() {
- const hasKey = !!apiKey;
- document.getElementById('noKeySection').classList.toggle('hidden', hasKey);
- document.getElementById('hasKeySection').classList.toggle('hidden', !hasKey);
- document.getElementById('uploadSection').classList.toggle('hidden', !hasKey);
- document.getElementById('filesSection').classList.toggle('hidden', !hasKey);
-
- if (hasKey) {
- document.getElementById('apiKeyDisplay').textContent = apiKey;
- loadFiles();
- }
-}
-
-async function generateKey() {
- const messageDiv = document.getElementById('authMessage');
- try {
- const response = await fetch('/add-user', {
- method: 'POST'
- });
- const result = await response.json();
-
- if (response.ok) {
- apiKey = result.api_key;
- localStorage.setItem('apiKey', apiKey);
- messageDiv.innerHTML = 'API key generated successfully!
';
- updateUI();
- } else {
- messageDiv.innerHTML = `${result.detail}
`;
- }
- } catch (error) {
- messageDiv.innerHTML = 'Failed to generate key
';
- }
-}
-
-function useExistingKey() {
- const key = document.getElementById('existingKey').value.trim();
- const messageDiv = document.getElementById('authMessage');
-
- if (!key) {
- messageDiv.innerHTML = 'Please enter an API key
';
- return;
- }
-
- apiKey = key;
- localStorage.setItem('apiKey', apiKey);
- messageDiv.innerHTML = 'API key set!
';
- document.getElementById('existingKey').value = '';
- updateUI();
-}
-
-function clearKey() {
- apiKey = null;
- localStorage.removeItem('apiKey');
- document.getElementById('authMessage').innerHTML = '';
- updateUI();
-}
-
-async function uploadFiles() {
- const input = document.getElementById('fileInput');
- const files = input.files;
- const messageDiv = document.getElementById('uploadMessage');
-
- if (files.length === 0) {
- messageDiv.innerHTML = 'Please select files
';
- return;
- }
-
- const formData = new FormData();
- for (let file of files) {
- formData.append('files', file);
- }
-
- try {
- const response = await fetch('/upload', {
- method: 'POST',
- headers: {
- 'X-API-Key': apiKey
- },
- body: formData
- });
- const result = await response.json();
-
- if (response.ok) {
- messageDiv.innerHTML = 'Files uploaded successfully!
';
- input.value = '';
- loadFiles();
- } else {
- messageDiv.innerHTML = `${result.detail}
`;
- }
- } catch (error) {
- messageDiv.innerHTML = 'Upload failed
';
- }
-}
-
-async function loadFiles() {
- if (!apiKey) return;
-
- try {
- const response = await fetch('/files', {
- headers: {
- 'X-API-Key': apiKey
- }
- });
- const files = await response.json();
- const fileList = document.getElementById('fileList');
-
- if (response.status === 401) {
- document.getElementById('authMessage').innerHTML = 'Invalid API key
';
- clearKey();
- return;
- }
-
- if (files.length === 0) {
- fileList.innerHTML = 'No files available';
- return;
- }
-
- fileList.innerHTML = files.map(file => `
-
-
-
${file.original_name} (${formatBytes(file.size)})
⬇
-
Uploaded: ${new Date(file.uploaded_at).toLocaleString()}
-
-
-
-
-
-
- `).join('');
- } catch (error) {
- console.error('Failed to load files:', error);
- }
-}
-
-async function downloadFile(fileId, filename) {
- window.location.href = `/download/${fileId}?api_key=${apiKey}`;
-}
-
-async function deleteFile(fileId) {
- if (!confirm('Are you sure you want to delete this file?')) return;
-
- try {
- const response = await fetch(`/files/${fileId}`, {
- method: 'DELETE',
- headers: {
- 'X-API-Key': apiKey
- }
- });
-
- if (response.ok) {
- loadFiles();
- } else {
- alert('Failed to delete file');
- }
- } catch (error) {
- alert('Failed to delete file');
- }
-}
-
-function formatBytes(bytes) {
- if (bytes === 0) return '0 Bytes';
- const k = 1024;
- const sizes = ['Bytes', 'KB', 'MB', 'GB'];
- const i = Math.floor(Math.log(bytes) / Math.log(k));
- return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
-}
-
-// Initialize UI
-updateUI();
\ No newline at end of file
diff --git a/main.py b/main.py
index a022b3e..226bc20 100644
--- a/main.py
+++ b/main.py
@@ -1,472 +1,752 @@
-'''
-🗄️ PostgreSQL Database Integration
-
-Database Tables:
-users - Stores user_id, api_key, and creation timestamp
-files - Stores file metadata with foreign key to users
-Automatic cascading delete when user is removed
-Indexed columns for fast lookups
-
-Stored Information:
-API keys and user IDs
-File metadata (name, size, path, upload date)
-User-file relationships
-
-📁 File Organization
-Directory Structure:
-uploads/
-├── user-id-1/
-│ ├── file-id-1_document.pdf
-│ └── file-id-2_image.jpg
-└── user-id-2/
- └── file-id-3_data.csv
-Each user gets their own subdirectory for better organization and isolation.
-
-⚙️ Setup Instructions
-
-Install dependencies:
-bashpip install fastapi uvicorn python-multipart asyncpg
-
-Set up PostgreSQL database:
-bash# Create database
-createdb fileserver
-
-# Set DATABASE_URL environment variable
-export DATABASE_URL="postgresql://username:password@localhost:5432/fileserver"
-
-Run the server:
-bashpython script_name.py
-
-The database tables will be created automatically on startup!
-🔑 Key Features
-
-✅ All data persists in PostgreSQL
-✅ Files organized in user-specific subdirectories
-✅ Automatic database initialization
-✅ Connection pooling for better performance
-✅ Proper foreign key relationships with cascade delete
-✅ Timestamps for all uploads
-
-The DATABASE_URL can be configured via environment variable for different environments (development, production, etc.).
-'''
-import random
-import string
-import sys
-from fastapi import FastAPI, File, UploadFile, HTTPException, Header, Depends
-from fastapi.responses import FileResponse, HTMLResponse
-from fastapi.security import APIKeyHeader
-from pathlib import Path
-import shutil
import os
-from typing import List, Optional
-import uuid
import secrets
-from datetime import datetime
-import asyncpg
-from contextlib import asynccontextmanager
+import hashlib
+import sys
+from datetime import datetime, timedelta
+from typing import Optional, List
+from pathlib import Path
+
+from fastapi import FastAPI, Depends, HTTPException, status, UploadFile, File, Form, Request, Cookie
+from fastapi.security import HTTPBasic, HTTPBasicCredentials, APIKeyHeader
+from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse
+from fastapi.templating import Jinja2Templates
+from fastapi.staticfiles import StaticFiles
+from sqlalchemy import create_engine, Column, Integer, String, DateTime, Boolean, ForeignKey, BigInteger, func
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.orm import sessionmaker, Session, relationship
+from pydantic import BaseModel, Field
+import uvicorn
+
import app_config
ERROR_STATUS_NO_DB_URL = 10
-STYLE_MACRO = "#$STYLE$#"
-SCRIPT_MACRO = "#$SCRIPT$#"
-# Database configuration
app_config.init_config()
+
+# Database Configuration
DATABASE_URL = app_config.DB_URL
-
-# Global database pool
-db_pool: asyncpg.Pool | None = None
-
-@asynccontextmanager
-async def lifespan(app: FastAPI):
- """Checking DB URL"""
- global DATABASE_URL
+if not DATABASE_URL:
+ DATABASE_URL = os.getenv("DATABASE_URL", "")
if not DATABASE_URL:
- DATABASE_URL = os.getenv("DATABASE_URL", "")
- if not DATABASE_URL:
- print('DB URL does not set. DB URL is a mandatory variable.\nPlease provide it in config file or as a environment variable and restart the server')
+ print('DB URL does not set. DB URL is a mandatory variable.\nPlease provide it in config file or as an environment variable and start the server again')
sys.exit(ERROR_STATUS_NO_DB_URL)
- """Manage application lifespan - startup and shutdown"""
- global db_pool
-
- # Startup: Create database pool and initialize tables
- db_pool = await asyncpg.create_pool(DATABASE_URL, min_size=2, max_size=10)
- await init_database()
- print("✅ Database connected and initialized")
-
- yield
-
- # Shutdown: Close database pool
- await db_pool.close()
- print("👋 Database connection closed")
-
-
-app = FastAPI(
- title="Das File Server",
- description="Upload, save, and download files with API key authentication",
- lifespan=lifespan,
- openapi_url="/api/v1/openapi.json" if app_config.IS_DOCS_AVAILABLE else None
-)
-
-# Configure upload directory
UPLOAD_DIR = Path(app_config.UPLOAD_DIR)
UPLOAD_DIR.mkdir(exist_ok=True)
-# API Key header
+# Templates
+templates = Jinja2Templates(directory="templates")
+
+engine = create_engine(DATABASE_URL)
+SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
+Base = declarative_base()
+
+# Security
+security = HTTPBasic()
api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
-
-async def init_database():
- """Initialize database tables"""
- async with db_pool.acquire() as conn:
- # Create users table
- await conn.execute("""
- CREATE TABLE IF NOT EXISTS users (
- user_id UUID PRIMARY KEY,
- api_key VARCHAR(255) UNIQUE NOT NULL,
- alias VARCHAR(32) UNIQUE NOT NULL,
- created_at TIMESTAMP DEFAULT NOW()
- )
- """)
-
- # Create files table
- await conn.execute("""
- CREATE TABLE IF NOT EXISTS files (
- file_id UUID PRIMARY KEY,
- user_id UUID NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
- original_name VARCHAR(500) NOT NULL,
- stored_name VARCHAR(500) NOT NULL,
- file_path TEXT NOT NULL,
- size BIGINT NOT NULL,
- uploaded_at TIMESTAMP DEFAULT NOW(),
- CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(user_id)
- )
- """)
-
- # Create index for faster lookups
- await conn.execute("""
- CREATE INDEX IF NOT EXISTS idx_files_user_id ON files(user_id)
- """)
- await conn.execute("""
- CREATE INDEX IF NOT EXISTS idx_users_api_key ON users(api_key)
- """)
+# Session storage (in-memory for demo, use Redis in production)
+sessions = {}
-async def get_user_id(api_key: Optional[str] = Depends(api_key_header)) -> str:
- """Validate API key and return user ID"""
- if not api_key:
- raise HTTPException(status_code=401, detail="Missing API key")
+# Create tables
+Base.metadata.create_all(bind=engine)
- async with db_pool.acquire() as conn:
- user = await conn.fetchrow(
- "SELECT user_id FROM users WHERE api_key = $1",
- api_key
+
+# Database Models
+class User(Base):
+ __tablename__ = "users"
+
+ id = Column(Integer, primary_key=True, index=True)
+ username = Column(String(50), unique=True, index=True, nullable=False)
+ password_hash = Column(String(256), nullable=False)
+ is_admin = Column(Boolean, default=False)
+ is_blocked = Column(Boolean, default=False)
+ api_key = Column(String(64), unique=True, index=True)
+ created_at = Column(DateTime, default=datetime.utcnow)
+
+ files = relationship("FileMetadata", back_populates="owner", cascade="all, delete-orphan")
+
+
+class FileMetadata(Base):
+ __tablename__ = "files"
+
+ id = Column(Integer, primary_key=True, index=True)
+ filename = Column(String(255), nullable=False)
+ stored_filename = Column(String(255), unique=True, nullable=False)
+ file_size = Column(BigInteger, nullable=False)
+ content_type = Column(String(100))
+ share_token = Column(String(64), unique=True, index=True)
+ user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
+ uploaded_at = Column(DateTime, default=datetime.utcnow)
+
+ owner = relationship("User", back_populates="files")
+
+
+# Pydantic Models
+class UserCreate(BaseModel):
+ username: str = Field(..., min_length=3, max_length=50)
+ password: str = Field(..., min_length=8)
+ is_admin: bool = False
+
+
+class UserResponse(BaseModel):
+ id: int
+ username: str
+ is_admin: bool
+ is_blocked: bool
+ created_at: datetime
+
+ class Config:
+ from_attributes = True
+
+
+class FileInfo(BaseModel):
+ id: int
+ filename: str
+ file_size: int
+ content_type: Optional[str]
+ uploaded_at: datetime
+ share_link: Optional[str]
+
+ class Config:
+ from_attributes = True
+
+# Password Hashing with PBKDF2-SHA256
+def hash_password(password: str) -> str:
+ """Hash password using PBKDF2-SHA256"""
+ salt = os.urandom(32)
+ pwdhash = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt, 100000)
+ return salt.hex() + ':' + pwdhash.hex()
+
+
+def verify_password(stored_password: str, provided_password: str) -> bool:
+ """Verify password against hash"""
+ try:
+ salt, pwdhash = stored_password.split(':')
+ return hashlib.pbkdf2_hmac(
+ 'sha256',
+ provided_password.encode('utf-8'),
+ bytes.fromhex(salt),
+ 100000
+ ).hex() == pwdhash
+ except Exception:
+ return False
+
+
+def generate_api_key() -> str:
+ """Generate a secure API key"""
+ return secrets.token_urlsafe(48)
+
+
+def generate_share_token() -> str:
+ """Generate a secure share token"""
+ return secrets.token_urlsafe(32)
+
+
+def generate_session_token() -> str:
+ """Generate a secure session token"""
+ return secrets.token_urlsafe(32)
+
+
+def format_size(size_bytes: int) -> str:
+ """Format file size in human-readable format"""
+ for unit in ['B', 'KB', 'MB', 'GB']:
+ if size_bytes < 1024.0:
+ return f"{size_bytes:.1f} {unit}"
+ size_bytes /= 1024.0
+ return f"{size_bytes:.1f} TB"
+
+
+# Add filter to Jinja2
+templates.env.filters['format_size'] = format_size
+
+
+# Database Dependency
+def get_db():
+ db = SessionLocal()
+ try:
+ yield db
+ finally:
+ db.close()
+
+
+# Session Management
+def get_session_user(session_token: Optional[str] = Cookie(None), db: Session = Depends(get_db)) -> Optional[User]:
+ """Get user from session token"""
+ if not session_token or session_token not in sessions:
+ return None
+
+ user_id = sessions[session_token]
+ user = db.query(User).filter(User.id == user_id).first()
+
+ if not user or user.is_blocked:
+ return None
+
+ return user
+
+
+# Authentication
+def get_current_user_http(
+ credentials: HTTPBasicCredentials = Depends(security),
+ db: Session = Depends(get_db)
+) -> User:
+ """Authenticate user via HTTP Basic Auth"""
+ user = db.query(User).filter(User.username == credentials.username).first()
+
+ if not user or not verify_password(user.password_hash, credentials.password):
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Invalid credentials",
+ headers={"WWW-Authenticate": "Basic"},
)
- if not user:
- raise HTTPException(status_code=401, detail="Invalid API key")
+ if user.is_blocked:
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="User account is blocked"
+ )
- return str(user['user_id'])
+ return user
-def get_user_directory(user_id: str) -> Path:
- """Get or create user's subdirectory"""
- user_dir = UPLOAD_DIR / user_id
- user_dir.mkdir(exist_ok=True)
- return user_dir
+def get_current_user_api(
+ api_key: Optional[str] = Depends(api_key_header),
+ db: Session = Depends(get_db)
+) -> User:
+ """Authenticate user via API Key"""
+ if not api_key:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="API Key required"
+ )
+
+ user = db.query(User).filter(User.api_key == api_key).first()
+
+ if not user:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Invalid API Key"
+ )
+
+ if user.is_blocked:
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="User account is blocked"
+ )
+
+ return user
-def get_file_content(file_name):
+def get_current_user(
+ session_user: Optional[User] = Depends(get_session_user),
+ api_key: Optional[str] = Depends(api_key_header),
+ db: Session = Depends(get_db)
+) -> User:
+ """Get current user from session or API authentication"""
+ if session_user:
+ return session_user
+
+ if api_key:
+ return get_current_user_api(api_key, db)
+
+ # Try HTTP Basic Auth
try:
- with open(file_name, 'r') as file:
- file_content = file.read()
- return file_content
- except FileNotFoundError:
- print(f"Error: The file '{file_name}' was not found.")
- except Exception as e:
- print(f"An error occurred: {e}")
+ from fastapi.security import HTTPBasicCredentials
+ # This is for API endpoints
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Authentication required"
+ )
+ except:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Authentication required"
+ )
+def require_admin(user: User = Depends(get_current_user)) -> User:
+ """Require admin role"""
+ if not user.is_admin:
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="Admin privileges required"
+ )
+ return user
+
+
+# FastAPI Application
+app = FastAPI(
+ title="File Storage Service by -=:dAs:=-",
+ description="Upload, save, download, share and remove files with for authenticated users included administrative dashboard and REST API",
+ openapi_url="/api/v1/openapi.json" if app_config.IS_DOCS_AVAILABLE else None,
+ version=app_config.APP_VERSION
+)
+
+
+# Initialize default admin user
+@app.on_event("startup")
+async def startup_event():
+ db = SessionLocal()
+ try:
+ admin = db.query(User).filter(User.username == "admin").first()
+ if not admin:
+ admin = User(
+ username="admin",
+ password_hash=hash_password("admin123"),
+ is_admin=True,
+ api_key=generate_api_key()
+ )
+ db.add(admin)
+ db.commit()
+ print(f"Default admin created. Username: admin, Password: admin123")
+ print(f"Admin API Key: {admin.api_key}")
+ finally:
+ db.close()
+
+
+# Web Routes
+
@app.get("/", response_class=HTMLResponse)
-async def home():
- """Serve the web interface"""
- css = get_file_content(app_config.CSS_FILE)
- js = get_file_content(app_config.JS_FILE)
- return (get_file_content(app_config.HTML_FILE)
- .replace(STYLE_MACRO, css)
- .replace(SCRIPT_MACRO, js))
+async def root(session_user: Optional[User] = Depends(get_session_user)):
+ """Root page - redirect to login or dashboard"""
+ if session_user:
+ return RedirectResponse(url="/dashboard", status_code=302)
+ return RedirectResponse(url="/login", status_code=302)
-@app.post("/new-key")
-@app.get("/new-key")
-async def generate_key():
- """Generate a new API key for a user"""
- api_key = secrets.token_urlsafe(32)
- try:
- return {
- "api_key": api_key,
- "message": "New API key generated successfully."
+@app.get("/login", response_class=HTMLResponse)
+async def login_page(request: Request, error: Optional[str] = None):
+ """Login page"""
+ return templates.TemplateResponse("login.html", {
+ "request": request,
+ "error": error
+ })
+
+
+@app.post("/login")
+async def login(
+ request: Request,
+ username: str = Form(...),
+ password: str = Form(...),
+ db: Session = Depends(get_db)
+):
+ """Process login"""
+ user = db.query(User).filter(User.username == username).first()
+
+ if not user or not verify_password(user.password_hash, password):
+ return templates.TemplateResponse("login.html", {
+ "request": request,
+ "error": "Invalid username or password"
+ })
+
+ if user.is_blocked:
+ return templates.TemplateResponse("login.html", {
+ "request": request,
+ "error": "Your account has been blocked"
+ })
+
+ # Create session
+ session_token = generate_session_token()
+ sessions[session_token] = user.id
+
+ response = RedirectResponse(url="/dashboard", status_code=302)
+ response.set_cookie(key="session_token", value=session_token, httponly=True)
+ return response
+
+
+@app.get("/logout")
+async def logout(session_token: Optional[str] = Cookie(None)):
+ """Logout"""
+ if session_token and session_token in sessions:
+ del sessions[session_token]
+
+ response = RedirectResponse(url="/login", status_code=302)
+ response.delete_cookie("session_token")
+ return response
+
+
+@app.get("/dashboard", response_class=HTMLResponse)
+async def dashboard(
+ request: Request,
+ user: User = Depends(get_session_user),
+ db: Session = Depends(get_db)
+):
+ """User dashboard"""
+ if not user:
+ return RedirectResponse(url="/login", status_code=302)
+
+ files = db.query(FileMetadata).filter(FileMetadata.user_id == user.id).order_by(
+ FileMetadata.uploaded_at.desc()).all()
+
+ return templates.TemplateResponse("user_files.html", {
+ "request": request,
+ "username": user.username,
+ "is_admin": user.is_admin,
+ "files": files,
+ "format_size": format_size
+ })
+
+
+@app.get("/settings", response_class=HTMLResponse)
+async def settings_page(
+ request: Request,
+ user: User = Depends(get_session_user),
+ db: Session = Depends(get_db)
+):
+ """Settings page"""
+ if not user:
+ return RedirectResponse(url="/login", status_code=302)
+
+ file_count = db.query(FileMetadata).filter(FileMetadata.user_id == user.id).count()
+
+ return templates.TemplateResponse("settings.html", {
+ "request": request,
+ "username": user.username,
+ "is_admin": user.is_admin,
+ "api_key": user.api_key,
+ "file_count": file_count
+ })
+
+
+@app.post("/settings/change-password")
+async def change_password(
+ request: Request,
+ current_password: str = Form(...),
+ new_password: str = Form(...),
+ confirm_password: str = Form(...),
+ user: User = Depends(get_session_user),
+ db: Session = Depends(get_db)
+):
+ """Change password"""
+ if not user:
+ return RedirectResponse(url="/login", status_code=302)
+
+ if not verify_password(user.password_hash, current_password):
+ file_count = db.query(FileMetadata).filter(FileMetadata.user_id == user.id).count()
+ return templates.TemplateResponse("settings.html", {
+ "request": request,
+ "username": user.username,
+ "is_admin": user.is_admin,
+ "api_key": user.api_key,
+ "file_count": file_count,
+ "error": "Current password is incorrect"
+ })
+
+ if new_password != confirm_password:
+ file_count = db.query(FileMetadata).filter(FileMetadata.user_id == user.id).count()
+ return templates.TemplateResponse("settings.html", {
+ "request": request,
+ "username": user.username,
+ "is_admin": user.is_admin,
+ "api_key": user.api_key,
+ "file_count": file_count,
+ "error": "New passwords do not match"
+ })
+
+ user.password_hash = hash_password(new_password)
+ db.commit()
+
+ file_count = db.query(FileMetadata).filter(FileMetadata.user_id == user.id).count()
+ return templates.TemplateResponse("settings.html", {
+ "request": request,
+ "username": user.username,
+ "is_admin": user.is_admin,
+ "api_key": user.api_key,
+ "file_count": file_count,
+ "message": "Password changed successfully"
+ })
+
+
+@app.get("/admin", response_class=HTMLResponse)
+async def admin_panel(
+ request: Request,
+ user: User = Depends(get_session_user),
+ db: Session = Depends(get_db)
+):
+ """Admin panel"""
+ if not user:
+ return RedirectResponse(url="/login", status_code=302)
+
+ if not user.is_admin:
+ return RedirectResponse(url="/dashboard", status_code=302)
+
+ # Get all users with file counts
+ users = db.query(
+ User,
+ func.count(FileMetadata.id).label('file_count')
+ ).outerjoin(FileMetadata).group_by(User.id).order_by(User.created_at.desc()).all()
+
+ users_list = []
+ for u, file_count in users:
+ user_dict = {
+ 'id': u.id,
+ 'username': u.username,
+ 'is_admin': u.is_admin,
+ 'is_blocked': u.is_blocked,
+ 'created_at': u.created_at,
+ 'file_count': file_count
}
- except Exception as e:
- raise HTTPException(status_code=500, detail=f"Failed to generate API key: {str(e)}")
+ users_list.append(type('User', (), user_dict)())
-
-@app.post("/new-alias")
-@app.get("/new-alias")
-async def generate_alias():
- """Generate a new alias"""
- return {
- "alias": await get_alias(),
- "message": "New Alias generated successfully."
+ # Statistics
+ stats = {
+ 'total_users': db.query(User).count(),
+ 'active_users': db.query(User).filter(User.is_blocked == False).count(),
+ 'blocked_users': db.query(User).filter(User.is_blocked == True).count(),
+ 'total_files': db.query(FileMetadata).count()
}
-async def get_all_aliases():
- async with db_pool.acquire() as conn:
- aliases = await conn.fetch("""
- SELECT alias FROM users
- """)
- return aliases
-
-async def get_alias():
- """Getting the alias"""
- existing_aliases = await get_all_aliases()
- alias = existing_aliases[0] if existing_aliases else ""
- while alias in existing_aliases or alias == "":
- num_letters = app_config.ALIAS_NUM_LETTERS
- num_digits = app_config.ALIAS_NUM_DIGITS
- letters = [random.choice(string.ascii_letters) for _ in range(num_letters)]
- digits = [random.choice(string.digits) for _ in range(num_digits)]
- res_list = letters + digits
- #random.shuffle(res_list)
- alias = ''.join(res_list)
- return alias
+ return templates.TemplateResponse("admin.html", {
+ "request": request,
+ "username": user.username,
+ "is_admin": user.is_admin,
+ "users": users_list,
+ "stats": stats
+ })
-@app.post("/add-user")
-async def add_new_user():
- """Add new user with new API key"""
- # Generate secure random API key
- api_key = secrets.token_urlsafe(32)
- user_id = uuid.uuid4()
- user_alias = get_alias()
+# API Endpoints
- async with db_pool.acquire() as conn:
- try:
- # Store API key in database
- await conn.execute(
- "INSERT INTO users (user_id, api_key) VALUES ($1, $2, $3)",
- user_id, api_key, user_alias
- )
-
- # Create user's directory
- get_user_directory(str(user_id))
-
- return {
- "api_key": api_key,
- "user_id": str(user_id),
- "message": "API key generated successfully. Keep it secure!"
- }
- except Exception as e:
- raise HTTPException(status_code=500, detail=f"Failed to generate API key: {str(e)}")
-
-
-@app.post("/upload")
-async def upload_files(
- files: List[UploadFile] = File(...),
- user_id: str = Depends(get_user_id)
+# User Management (Admin Only)
+@app.post("/api/users", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
+async def create_user(
+ user_data: UserCreate,
+ db: Session = Depends(get_db),
+ admin: User = Depends(require_admin)
):
- """Upload one or multiple files (requires API key)"""
- uploaded_files = []
- user_dir = get_user_directory(user_id)
+ """Create a new user (Admin only)"""
+ existing = db.query(User).filter(User.username == user_data.username).first()
+ if existing:
+ raise HTTPException(status_code=400, detail="Username already exists")
- async with db_pool.acquire() as conn:
- for file in files:
- # Generate unique ID for file
- file_id = uuid.uuid4()
- stored_name = f"{file_id}_{file.filename}"
- file_path = user_dir / stored_name
+ new_user = User(
+ username=user_data.username,
+ password_hash=hash_password(user_data.password),
+ is_admin=user_data.is_admin,
+ api_key=generate_api_key()
+ )
- # Save file
- try:
- with open(file_path, "wb") as buffer:
- shutil.copyfileobj(file.file, buffer)
+ db.add(new_user)
+ db.commit()
+ db.refresh(new_user)
- file_size = os.path.getsize(file_path)
-
- # Store metadata in database
- await conn.execute("""
- INSERT INTO files (file_id, user_id, original_name, stored_name, file_path, size)
- VALUES ($1, $2, $3, $4, $5, $6)
- """, file_id, uuid.UUID(user_id), file.filename, stored_name, str(file_path), file_size)
-
- uploaded_files.append({
- "id": str(file_id),
- "filename": file.filename,
- "size": file_size
- })
- except Exception as e:
- # Clean up file if database insert fails
- if file_path.exists():
- os.remove(file_path)
- raise HTTPException(status_code=500, detail=f"Failed to upload {file.filename}: {str(e)}")
-
- return {"message": "Files uploaded successfully", "files": uploaded_files}
+ return new_user
-@app.get("/files")
-async def list_files(user_id: str = Depends(get_user_id)):
- """List all files for the authenticated user"""
- async with db_pool.acquire() as conn:
- files = await conn.fetch("""
- SELECT file_id, original_name, size, uploaded_at
- FROM files
- WHERE user_id = $1
- ORDER BY uploaded_at DESC
- """, uuid.UUID(user_id))
-
- return [
- {
- "file_id": str(file['file_id']),
- "original_name": file['original_name'],
- "size": file['size'],
- "uploaded_at": file['uploaded_at'].isoformat()
- }
- for file in files
- ]
+@app.get("/api/users", response_model=List[UserResponse])
+async def list_users(
+ db: Session = Depends(get_db),
+ admin: User = Depends(require_admin)
+):
+ """List all users (Admin only)"""
+ users = db.query(User).all()
+ return users
-@app.get("/download/{file_id}")
+@app.delete("/api/users/{user_id}")
+async def delete_user(
+ user_id: int,
+ db: Session = Depends(get_db),
+ admin: User = Depends(require_admin)
+):
+ """Delete a user (Admin only)"""
+ if user_id == admin.id:
+ raise HTTPException(status_code=400, detail="Cannot delete yourself")
+
+ user = db.query(User).filter(User.id == user_id).first()
+ if not user:
+ raise HTTPException(status_code=404, detail="User not found")
+
+ db.delete(user)
+ db.commit()
+
+ return {"message": "User deleted successfully"}
+
+
+@app.patch("/api/users/{user_id}/block")
+async def toggle_block_user(
+ user_id: int,
+ block: bool,
+ db: Session = Depends(get_db),
+ admin: User = Depends(require_admin)
+):
+ """Block or unblock a user (Admin only)"""
+ if user_id == admin.id:
+ raise HTTPException(status_code=400, detail="Cannot block yourself")
+
+ user = db.query(User).filter(User.id == user_id).first()
+ if not user:
+ raise HTTPException(status_code=404, detail="User not found")
+
+ user.is_blocked = block
+ db.commit()
+
+ return {"message": f"User {'blocked' if block else 'unblocked'} successfully"}
+
+
+@app.get("/api/users/me/api-key")
+async def get_my_api_key(user: User = Depends(get_current_user)):
+ """Get your API key"""
+ return {"api_key": user.api_key}
+
+
+@app.post("/api/users/me/api-key/regenerate")
+async def regenerate_api_key(
+ user: User = Depends(get_current_user),
+ db: Session = Depends(get_db)
+):
+ """Regenerate your API key"""
+ user.api_key = generate_api_key()
+ db.commit()
+ return {"api_key": user.api_key}
+
+
+# File Management
+@app.post("/api/files/upload", response_model=FileInfo)
+async def upload_file(
+ file: UploadFile = File(...),
+ user: User = Depends(get_current_user),
+ db: Session = Depends(get_db)
+):
+ """Upload a file"""
+ # Generate unique stored filename
+ stored_filename = f"{secrets.token_urlsafe(16)}_{file.filename}"
+ file_path = UPLOAD_DIR / stored_filename
+
+ # Save file
+ content = await file.read()
+ with open(file_path, "wb") as f:
+ f.write(content)
+
+ # Create metadata
+ file_metadata = FileMetadata(
+ filename=file.filename,
+ stored_filename=stored_filename,
+ file_size=len(content),
+ content_type=file.content_type,
+ share_token=generate_share_token(),
+ user_id=user.id
+ )
+
+ db.add(file_metadata)
+ db.commit()
+ db.refresh(file_metadata)
+
+ return FileInfo(
+ id=file_metadata.id,
+ filename=file_metadata.filename,
+ file_size=file_metadata.file_size,
+ content_type=file_metadata.content_type,
+ uploaded_at=file_metadata.uploaded_at,
+ share_link=f"/share/{file_metadata.share_token}"
+ )
+
+
+@app.get("/api/files", response_model=List[FileInfo])
+async def list_files(
+ user: User = Depends(get_current_user),
+ db: Session = Depends(get_db)
+):
+ """List user's files"""
+ files = db.query(FileMetadata).filter(FileMetadata.user_id == user.id).all()
+
+ return [
+ FileInfo(
+ id=f.id,
+ filename=f.filename,
+ file_size=f.file_size,
+ content_type=f.content_type,
+ uploaded_at=f.uploaded_at,
+ share_link=f"/share/{f.share_token}"
+ )
+ for f in files
+ ]
+
+
+@app.get("/api/files/{file_id}/download")
async def download_file(
- file_id: str,
- api_key: Optional[str] = None,
- x_api_key: Optional[str] = Header(None)
+ file_id: int,
+ user: User = Depends(get_current_user),
+ db: Session = Depends(get_db)
):
- """Download a file by ID (requires API key)"""
- # Accept API key from query param or header
- key = api_key or x_api_key
+ """Download a file"""
+ file_metadata = db.query(FileMetadata).filter(
+ FileMetadata.id == file_id,
+ FileMetadata.user_id == user.id
+ ).first()
- if not key:
- raise HTTPException(status_code=401, detail="Missing API key")
+ if not file_metadata:
+ raise HTTPException(status_code=404, detail="File not found")
- async with db_pool.acquire() as conn:
- # Verify API key and get user
- user = await conn.fetchrow(
- "SELECT user_id FROM users WHERE api_key = $1",
- key
- )
+ file_path = UPLOAD_DIR / file_metadata.stored_filename
+ if not file_path.exists():
+ raise HTTPException(status_code=404, detail="File not found on disk")
- if not user:
- raise HTTPException(status_code=401, detail="Invalid API key")
-
- # Get file info and verify ownership
- file_info = await conn.fetchrow("""
- SELECT file_path, original_name
- FROM files
- WHERE file_id = $1 AND user_id = $2
- """, uuid.UUID(file_id), user['user_id'])
-
- if not file_info:
- raise HTTPException(status_code=404, detail="File not found or access denied")
-
- file_path = Path(file_info['file_path'])
-
- if not file_path.exists():
- raise HTTPException(status_code=404, detail="File not found on disk")
-
- return FileResponse(
- path=file_path,
- filename=file_info['original_name'],
- media_type="application/octet-stream"
- )
+ from fastapi.responses import FileResponse as FastAPIFileResponse
+ return FastAPIFileResponse(
+ path=str(file_path),
+ filename=file_metadata.filename,
+ media_type=file_metadata.content_type
+ )
-@app.get("/dn/{file_id}")
-async def download_link(
- file_id: str
-):
- """Download a file by DIRECT LINK (without API key)"""
- async with db_pool.acquire() as conn:
- # Get file info and verify ownership
- file_info = await conn.fetchrow("""
- SELECT file_path, original_name
- FROM files
- WHERE file_id = $1
- """, uuid.UUID(file_id))
-
- if not file_info:
- raise HTTPException(status_code=404, detail="File not found or access denied")
-
- file_path = Path(file_info['file_path'])
-
- if not file_path.exists():
- raise HTTPException(status_code=404, detail="File not found on disk")
-
- return FileResponse(
- path=file_path,
- filename=file_info['original_name'],
- media_type="application/octet-stream"
- )
-
-
-@app.delete("/files/{file_id}")
+@app.delete("/api/files/{file_id}")
async def delete_file(
- file_id: str,
- user_id: str = Depends(get_user_id)
+ file_id: int,
+ user: User = Depends(get_current_user),
+ db: Session = Depends(get_db)
):
- """Delete a file by ID (only owner can delete)"""
- async with db_pool.acquire() as conn:
- # Get file info and verify ownership
- file_info = await conn.fetchrow("""
- SELECT file_path
- FROM files
- WHERE file_id = $1 AND user_id = $2
- """, uuid.UUID(file_id), uuid.UUID(user_id))
+ """Delete a file"""
+ file_metadata = db.query(FileMetadata).filter(
+ FileMetadata.id == file_id,
+ FileMetadata.user_id == user.id
+ ).first()
- if not file_info:
- raise HTTPException(status_code=404, detail="File not found or access denied")
+ if not file_metadata:
+ raise HTTPException(status_code=404, detail="File not found")
- file_path = Path(file_info['file_path'])
+ # Delete physical file
+ file_path = UPLOAD_DIR / file_metadata.stored_filename
+ if file_path.exists():
+ file_path.unlink()
- # Delete file from disk
- if file_path.exists():
- os.remove(file_path)
-
- # Remove from database
- await conn.execute(
- "DELETE FROM files WHERE file_id = $1",
- uuid.UUID(file_id)
- )
+ # Delete metadata
+ db.delete(file_metadata)
+ db.commit()
return {"message": "File deleted successfully"}
-@app.get("/files/{file_id}/info")
-async def get_file_info(
- file_id: str,
- user_id: str = Depends(get_user_id)
-):
- """Get file information by ID (only owner can view)"""
- async with db_pool.acquire() as conn:
- file_info = await conn.fetchrow("""
- SELECT file_id, original_name, stored_name, size, uploaded_at
- FROM files
- WHERE file_id = $1 AND user_id = $2
- """, uuid.UUID(file_id), uuid.UUID(user_id))
+# Public Share Endpoint
+@app.get("/share/{share_token}")
+async def download_shared_file(share_token: str, db: Session = Depends(get_db)):
+ """Download a shared file (no authentication required)"""
+ file_metadata = db.query(FileMetadata).filter(
+ FileMetadata.share_token == share_token
+ ).first()
- if not file_info:
- raise HTTPException(status_code=404, detail="File not found or access denied")
+ if not file_metadata:
+ raise HTTPException(status_code=404, detail="File not found")
- return {
- "file_id": str(file_info['file_id']),
- "original_name": file_info['original_name'],
- "stored_name": file_info['stored_name'],
- "size": file_info['size'],
- "uploaded_at": file_info['uploaded_at'].isoformat()
- }
+ file_path = UPLOAD_DIR / file_metadata.stored_filename
+ if not file_path.exists():
+ raise HTTPException(status_code=404, detail="File not found on disk")
+
+ from fastapi.responses import FileResponse as FastAPIFileResponse
+ return FastAPIFileResponse(
+ path=str(file_path),
+ filename=file_metadata.filename,
+ media_type=file_metadata.content_type
+ )
+
+
+# Health Check
+@app.get("/health")
+async def health_check():
+ """Health check endpoint"""
+ return {"status": "healthy"}
if __name__ == "__main__":
- import uvicorn
-
uvicorn.run(app, host="0.0.0.0", port=app_config.APP_PORT)
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 5053b89..2d94f48 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,6 +1,9 @@
-fastapi~=0.118.0
-uvicorn~=0.37.0
-python-multipart
-fastapi[standard]
-asyncpg~=0.30.0
-PyYAML~=6.0.3
\ No newline at end of file
+fastapi==0.104.1
+uvicorn[standard]==0.24.0
+sqlalchemy==2.0.23
+psycopg2-binary==2.9.9
+python-multipart==0.0.19
+pydantic==2.5.0
+pydantic-settings==2.1.0
+python-dotenv==1.0.0
+jinja2==3.1.6
\ No newline at end of file
diff --git a/setup.sh b/setup.sh
new file mode 100644
index 0000000..522a7f1
--- /dev/null
+++ b/setup.sh
@@ -0,0 +1,72 @@
+#!/bin/bash
+
+echo "🚀 Setting up Das File Storage Application..."
+
+# Create necessary directories
+echo "📁 Creating directories..."
+mkdir -p uploads
+mkdir -p templates
+
+# Check if templates exist
+if [ ! -f "templates/base.html" ]; then
+ echo "⚠️ Warning: Template files not found in templates/ directory"
+ echo "Please make sure to copy all HTML template files to the templates/ directory:"
+ echo " - base.html"
+ echo " - login.html"
+ echo " - user_files.html"
+ echo " - settings.html"
+ echo " - admin.html"
+fi
+
+# Create .env if it doesn't exist
+if [ ! -f ".env" ]; then
+ echo "📝 Creating .env file..."
+ cp .env.example .env
+ echo "✅ Created .env file. Please update with your database credentials."
+fi
+
+# Check if Python is installed
+if ! command -v python3 &> /dev/null; then
+ echo "❌ Python 3 is not installed. Please install Python 3.8 or higher."
+ exit 1
+fi
+
+# Check if PostgreSQL is running (optional check)
+if command -v pg_isready &> /dev/null; then
+ if pg_isready &> /dev/null; then
+ echo "✅ PostgreSQL is running"
+ else
+ echo "⚠️ PostgreSQL is not running. Please start PostgreSQL."
+ fi
+fi
+
+# Create virtual environment
+if [ ! -d "venv" ]; then
+ echo "🐍 Creating virtual environment..."
+ python3 -m venv venv
+fi
+
+# Activate virtual environment
+echo "🔧 Activating virtual environment..."
+source venv/bin/activate
+
+# Install requirements
+echo "📦 Installing Python packages..."
+pip install --upgrade pip
+pip install -r requirements.txt
+
+echo ""
+echo "✅ Setup complete!"
+echo ""
+echo "📋 Next steps:"
+echo "1. Update .env file with your PostgreSQL credentials"
+echo "2. Make sure all template files are in the templates/ directory"
+echo "3. Create PostgreSQL database: createdb filestore"
+echo "4. Run the application: python main.py"
+echo "5. Open browser: http://localhost:8000"
+echo "6. Login with default admin credentials:"
+echo " Username: admin"
+echo " Password: admin123"
+echo ""
+echo "⚠️ Remember to change the admin password immediately!"
+echo ""
\ No newline at end of file
diff --git a/templates/admin.html b/templates/admin.html
new file mode 100644
index 0000000..5e85aaf
--- /dev/null
+++ b/templates/admin.html
@@ -0,0 +1,266 @@
+{% extends "base.html" %}
+
+{% block title %}Admin Panel - Das File Storage{% endblock %}
+
+{% block content %}
+
+
+{% if message %}
+
+ {{ message }}
+
+{% endif %}
+
+{% if error %}
+
+ {{ error }}
+
+{% endif %}
+
+
+
User Management
+
+
+
+
+
📊 Statistics
+
+
+
Total Users
+
{{ stats.total_users }}
+
+
+
Active Users
+
{{ stats.active_users }}
+
+
+
Blocked Users
+
{{ stats.blocked_users }}
+
+
+
Total Files
+
{{ stats.total_files }}
+
+
+
+
+
+
+
+ | Username |
+ Role |
+ Files |
+ Created |
+ Status |
+ Actions |
+
+
+
+ {% for user in users %}
+
+ |
+ {{ user.username }}
+ |
+
+ {% if user.is_admin %}
+ Admin
+ {% else %}
+ User
+ {% endif %}
+ |
+ {{ user.file_count }} files |
+ {{ user.created_at.strftime('%Y-%m-%d') }} |
+
+ {% if user.is_blocked %}
+ 🚫 Blocked
+ {% else %}
+ ✅ Active
+ {% endif %}
+ |
+
+ {% if user.username != username %}
+ {% if user.is_blocked %}
+
+ {% else %}
+
+ {% endif %}
+
+ {% else %}
+ Current User
+ {% endif %}
+ |
+
+ {% endfor %}
+
+
+
+
+
+
+{% endblock %}
+
+{% block extra_scripts %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/templates/base.html b/templates/base.html
new file mode 100644
index 0000000..94a7ab1
--- /dev/null
+++ b/templates/base.html
@@ -0,0 +1,334 @@
+
+
+
+
+
+ {% block title %}Das File Storage{% endblock %}
+
+ {% block extra_style %}{% endblock %}
+
+
+
+
+
+
+ {% block content %}{% endblock %}
+
+
+
+ {% block extra_scripts %}{% endblock %}
+
+
\ No newline at end of file
diff --git a/templates/login.html b/templates/login.html
new file mode 100644
index 0000000..1077af1
--- /dev/null
+++ b/templates/login.html
@@ -0,0 +1,139 @@
+
+
+
+
+
+ Das File Storage
+
+
+
+
+
+
+ {% if error %}
+
+ {{ error }}
+
+ {% endif %}
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/settings.html b/templates/settings.html
new file mode 100644
index 0000000..747aa06
--- /dev/null
+++ b/templates/settings.html
@@ -0,0 +1,134 @@
+{% extends "base.html" %}
+
+{% block title %}Settings - Das File Storage{% endblock %}
+
+{% block content %}
+
+
+{% if message %}
+
+ {{ message }}
+
+{% endif %}
+
+{% if error %}
+
+ {{ error }}
+
+{% endif %}
+
+Account Settings
+
+
+
+
+
+
+
+
🔑 API Key
+
+ Use this key for API authentication with the X-API-Key header.
+
+
+
+
+
+ ⚠️ Warning: Regenerating will invalidate your current API key. All applications using the old key will stop working.
+
+
+
+
+
+
+
+
👤 Account Information
+
+
+
Username:
+
{{ username }}
+
+
+
Account Type:
+
+ {% if is_admin %}
+ Administrator
+ {% else %}
+ Regular User
+ {% endif %}
+
+
+
+
Total Files:
+
{{ file_count }} files
+
+
+
+
+
+{% endblock %}
+
+{% block extra_scripts %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/templates/user_files.html b/templates/user_files.html
new file mode 100644
index 0000000..cd193f0
--- /dev/null
+++ b/templates/user_files.html
@@ -0,0 +1,228 @@
+{% extends "base.html" %}
+
+{% block title %}My Files - Das File Storage{% endblock %}
+
+{% block content %}
+
+
+{% if message %}
+
+ {{ message }}
+
+{% endif %}
+
+{% if error %}
+
+ {{ error }}
+
+{% endif %}
+
+
+
My Files
+
+
+
+{% if files %}
+
+
+
+ | Filename |
+ Size |
+ Uploaded |
+ Actions |
+
+
+
+ {% for file in files %}
+
+ |
+ {{ file.filename }}
+ {{ file.content_type or 'Unknown' }}
+ |
+ {{ format_size(file.file_size) }} |
+ {{ file.uploaded_at.strftime('%Y-%m-%d %H:%M') }} |
+
+
+
+
+ |
+
+ {% endfor %}
+
+
+{% else %}
+
+
+
No files yet
+
Upload your first file to get started
+
+{% endif %}
+
+
+
+
+
+
+
+
+
Anyone with this link can download the file:
+
+
+
+
+
+
+
+
+
+
+{% endblock %}
+
+{% block extra_scripts %}
+
+{% endblock %}
\ No newline at end of file