Compare commits
No commits in common. "1618a5a0376618621c8934335280c9af7b987775" and "8d4307b1a7979931b718d10b2538332831f12759" have entirely different histories.
1618a5a037
...
8d4307b1a7
BIN
2024_seasonal_wallpapers_pack.zip
Normal file
BIN
2024_seasonal_wallpapers_pack.zip
Normal file
Binary file not shown.
193
AGENTS.md
193
AGENTS.md
@ -1,15 +1,14 @@
|
|||||||
# AGENTS.md — Image Server Project
|
# AGENTS.md - File Server Project
|
||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
A FastAPI-based image server that serves files from directories or ZIP archives using hashed paths for secure, randomized access. Supports password-protected ZIPs via HTTP Basic Auth. Provides a full-screen image viewer with navigation controls and auto-advance.
|
This is a FastAPI-based file server that serves files from a directory or ZIP archive using hashed paths. The server indexes files and serves them via random access or by hash lookup.
|
||||||
|
|
||||||
## Language & Runtime
|
## Language & Runtime
|
||||||
|
|
||||||
- **Language**: Python 3.13
|
- **Language**: Python 3.13
|
||||||
- **Framework**: FastAPI 0.128.0+
|
- **Framework**: FastAPI 0.128.0+
|
||||||
- **Server**: Uvicorn 0.40.0+
|
- **Server**: Uvicorn 0.40.0+
|
||||||
- **Package manager**: UV
|
|
||||||
|
|
||||||
## Build, Lint, and Test Commands
|
## Build, Lint, and Test Commands
|
||||||
|
|
||||||
@ -21,26 +20,31 @@ uv run main.py /path/to/files
|
|||||||
|
|
||||||
# Run with a ZIP archive
|
# Run with a ZIP archive
|
||||||
uv run main.py /path/to/archive.zip
|
uv run main.py /path/to/archive.zip
|
||||||
|
|
||||||
# Run with a glob pattern
|
|
||||||
uv run main.py "path/to/zips/*.zip"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
|
|
||||||
|
No test framework is currently configured. To add tests, install pytest:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run all tests
|
uv add pytest
|
||||||
uv run pytest
|
```
|
||||||
|
|
||||||
# Run a single test
|
Run all tests:
|
||||||
uv run pytest tests/test_file_indexer.py::TestClass::test_method
|
```bash
|
||||||
|
pytest
|
||||||
|
```
|
||||||
|
|
||||||
# Run tests matching a pattern
|
Run a single test:
|
||||||
uv run pytest -k "test_name_pattern"
|
```bash
|
||||||
|
pytest path/to/test_file.py::TestClass::test_method
|
||||||
|
pytest -k "test_name_pattern"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Linting
|
### Linting
|
||||||
|
|
||||||
|
No linting tool is currently configured. Recommended tools:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install ruff (fast linter)
|
# Install ruff (fast linter)
|
||||||
uv add ruff
|
uv add ruff
|
||||||
@ -74,101 +78,6 @@ uv add mypy
|
|||||||
uv run mypy main.py
|
uv run mypy main.py
|
||||||
```
|
```
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
image_server/
|
|
||||||
├── main.py # Main application — all server logic, routing, indexing
|
|
||||||
├── frontend.html # Single-page HTML template for the image viewer UI
|
|
||||||
├── pyproject.toml # Project configuration (dependencies, metadata)
|
|
||||||
├── uv.lock # UV lockfile for reproducible builds
|
|
||||||
├── README.md # User-facing documentation
|
|
||||||
├── AGENTS.md # This file — agent/dev documentation
|
|
||||||
└── tests/ # Test suite
|
|
||||||
├── conftest.py # Pytest fixtures and configuration
|
|
||||||
├── test_file_indexer.py # Tests for FileIndexer class
|
|
||||||
├── test_zip_indexer.py # Tests for ZipFileIndexer class
|
|
||||||
├── test_encrypted_zip.py # Tests for password-protected ZIP handling
|
|
||||||
├── test_endpoints.py # Tests for API endpoints
|
|
||||||
├── test_navigation.py # Tests for navigation logic
|
|
||||||
└── testfile.zip # Test fixture ZIP file
|
|
||||||
```
|
|
||||||
|
|
||||||
## Code Architecture
|
|
||||||
|
|
||||||
### Entry Point (`main.py`)
|
|
||||||
|
|
||||||
All code lives in `main.py`. The app is initialized via `initialize_server()` which is called from `__main__`.
|
|
||||||
|
|
||||||
### Global State
|
|
||||||
|
|
||||||
```python
|
|
||||||
file_mapping = {} # dict[str, str] — hash -> filepath/filename (merged across all indexers)
|
|
||||||
indexers = [] # list[FileIndexer] — all active indexers
|
|
||||||
```
|
|
||||||
|
|
||||||
### Indexer Classes
|
|
||||||
|
|
||||||
#### `FileIndexer` (base class)
|
|
||||||
|
|
||||||
Indexes files from a directory tree.
|
|
||||||
|
|
||||||
- **`__init__(path: str, salt: str | None)`** — Initializes with a path and optional salt
|
|
||||||
- **`salt` (property)** — Lazily generates a random salt if not provided
|
|
||||||
- **`_hash_path(filepath: str) -> str`** — SHA-256 hash of `filepath + salt`
|
|
||||||
- **`_index() -> dict[str, str]`** — Walks directory tree, builds hash -> filepath mapping
|
|
||||||
- **`get_file_by_hash(file_hash: str)`** — Yields file content bytes (generator)
|
|
||||||
- **`get_filename_by_hash(file_hash: str) -> str | None`** — Returns filepath by hash
|
|
||||||
|
|
||||||
#### `ZipFileIndexer(FileIndexer)`
|
|
||||||
|
|
||||||
Indexes files from a ZIP archive. Extends `FileIndexer`.
|
|
||||||
|
|
||||||
- **`_open_zip()`** — Opens the ZIP file for reading
|
|
||||||
- **`_index() -> dict[str, str]`** — Indexes files within the ZIP (hash -> internal filename)
|
|
||||||
- **`_needs_password(filename: str) -> bool`** — Lazy detection of password protection (cached)
|
|
||||||
- **`is_file_encrypted(file_hash: str) -> bool`** — Check if a file requires a password
|
|
||||||
- **`get_file_by_hash(file_hash: str, password: bytes | None)`** — Yields file content, handles encryption
|
|
||||||
- **`get_filename_by_hash(file_hash: str) -> str | None`** — Returns internal ZIP filename by hash
|
|
||||||
|
|
||||||
#### `INDEXER_MAP`
|
|
||||||
|
|
||||||
```python
|
|
||||||
INDEXER_MAP = {".zip": ZipFileIndexer}
|
|
||||||
```
|
|
||||||
|
|
||||||
Maps file extensions to indexer classes. Extend this dict to support new archive formats.
|
|
||||||
|
|
||||||
### Helper Functions
|
|
||||||
|
|
||||||
- **`initialize_server(args: argparse.Namespace)`** — Bootstraps the server: creates indexers, populates `file_mapping`
|
|
||||||
- **`_find_indexer_for_hash(file_hash: str)`** — Finds which indexer owns a given hash
|
|
||||||
- **`_get_zip_password(file_hash: str, request: Request) -> bytes | None`** — Extracts password from HTTP Basic Auth header
|
|
||||||
- **`_raise_unauthorized()`** — Raises HTTP 401 with `WWW-Authenticate: Basic` header
|
|
||||||
- **`_get_random_hash() -> str`** — Returns a random hash from the indexed files
|
|
||||||
- **`_build_url(file_hash, order, delay) -> str`** — Builds a URL with optional query params
|
|
||||||
- **`_get_navigation_data(file_hash, order)`** — Returns dict with `file_hash`, `next_hash`, `prev_hash`, `filename`
|
|
||||||
- **`_render_page(navigation_data, ...)`** — Renders `frontend.html` template with substituted values
|
|
||||||
|
|
||||||
### Routes
|
|
||||||
|
|
||||||
| Route | Handler | Description |
|
|
||||||
|-------|---------|-------------|
|
|
||||||
| `GET /` | `root()` | Redirect to random file |
|
|
||||||
| `GET /{file_hash}` | `hash_page()` | Serve image page with navigation |
|
|
||||||
| `GET /api/{file_hash}/data` | `get_file_data()` | Serve raw file data |
|
|
||||||
| `GET /api/health` | `health_check()` | Health check (status + file count) |
|
|
||||||
|
|
||||||
### Frontend Template (`frontend.html`)
|
|
||||||
|
|
||||||
Single HTML file using `string.Template` substitution. Placeholders:
|
|
||||||
- `$img_url` — URL to fetch the image data
|
|
||||||
- `$image_click_url` — URL navigated to on image click
|
|
||||||
- `$next_url` / `$prev_url` — Navigation URLs
|
|
||||||
- `$filename` — Display filename
|
|
||||||
- `$extra_meta` — Extra `<meta>` tags (e.g., auto-refresh)
|
|
||||||
- `$play_button` — Play/pause button HTML
|
|
||||||
|
|
||||||
## Code Style Guidelines
|
## Code Style Guidelines
|
||||||
|
|
||||||
### Imports
|
### Imports
|
||||||
@ -176,6 +85,17 @@ Single HTML file using `string.Template` substitution. Placeholders:
|
|||||||
- Standard library imports first, then third-party, then local
|
- Standard library imports first, then third-party, then local
|
||||||
- Group imports by type: stdlib, third-party, local
|
- Group imports by type: stdlib, third-party, local
|
||||||
- Use absolute imports for local modules
|
- Use absolute imports for local modules
|
||||||
|
- Example:
|
||||||
|
```python
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from fastapi import FastAPI, HTTPException
|
||||||
|
from fastapi.responses import FileResponse, StreamingResponse
|
||||||
|
|
||||||
|
from mymodule import MyClass
|
||||||
|
```
|
||||||
|
|
||||||
### Formatting Style
|
### Formatting Style
|
||||||
|
|
||||||
@ -189,6 +109,15 @@ Single HTML file using `string.Template` substitution. Placeholders:
|
|||||||
|
|
||||||
- Use type hints for all function arguments and return values
|
- Use type hints for all function arguments and return values
|
||||||
- Use Python 3.13+ typing features where appropriate
|
- Use Python 3.13+ typing features where appropriate
|
||||||
|
- Example:
|
||||||
|
```python
|
||||||
|
def process_file(path: str) -> bytes:
|
||||||
|
...
|
||||||
|
|
||||||
|
class FileIndexer:
|
||||||
|
def __init__(self, path: str) -> None:
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
### Naming Conventions
|
### Naming Conventions
|
||||||
|
|
||||||
@ -203,6 +132,14 @@ Single HTML file using `string.Template` substitution. Placeholders:
|
|||||||
- Use exceptions for exceptional cases, not flow control
|
- Use exceptions for exceptional cases, not flow control
|
||||||
- Raise `HTTPException` for HTTP-level errors in FastAPI endpoints
|
- Raise `HTTPException` for HTTP-level errors in FastAPI endpoints
|
||||||
- Provide meaningful error messages
|
- Provide meaningful error messages
|
||||||
|
- Example:
|
||||||
|
```python
|
||||||
|
if not indexer.file_mapping:
|
||||||
|
raise HTTPException(status_code=404, detail="No files indexed")
|
||||||
|
|
||||||
|
if file_hash not in self.file_mapping:
|
||||||
|
return None
|
||||||
|
```
|
||||||
|
|
||||||
### Async/Await
|
### Async/Await
|
||||||
|
|
||||||
@ -219,7 +156,18 @@ Single HTML file using `string.Template` substitution. Placeholders:
|
|||||||
### Documentation
|
### Documentation
|
||||||
|
|
||||||
- Use docstrings for public classes and functions
|
- Use docstrings for public classes and functions
|
||||||
- Follow Google-style docstring format
|
- Follow Google-style docstring format:
|
||||||
|
```python
|
||||||
|
def get_file_by_hash(file_hash: str) -> str | None:
|
||||||
|
"""Get filename by hash.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_hash: The hash identifier for the file.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The filename if found, None otherwise.
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
### Best Practices
|
### Best Practices
|
||||||
|
|
||||||
@ -229,6 +177,16 @@ Single HTML file using `string.Template` substitution. Placeholders:
|
|||||||
- Use dataclasses or attrs for simple data containers
|
- Use dataclasses or attrs for simple data containers
|
||||||
- Follow PEP 8 style guidelines
|
- Follow PEP 8 style guidelines
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
/projects/file_server/
|
||||||
|
├── main.py # Main application entry point
|
||||||
|
├── pyproject.toml # Project configuration
|
||||||
|
├── .python-version # Python version specification
|
||||||
|
└── README.md # Project documentation (currently empty)
|
||||||
|
```
|
||||||
|
|
||||||
## Common Tasks
|
## Common Tasks
|
||||||
|
|
||||||
### Adding a New Endpoint
|
### Adding a New Endpoint
|
||||||
@ -241,23 +199,12 @@ Single HTML file using `string.Template` substitution. Placeholders:
|
|||||||
### Adding a New File Indexer
|
### Adding a New File Indexer
|
||||||
|
|
||||||
1. Create a new class inheriting from `FileIndexer`
|
1. Create a new class inheriting from `FileIndexer`
|
||||||
2. Override the `_index` method to populate `self._file_mapping`
|
2. Override the `_index` method to populate `self.file_mapping`
|
||||||
3. Override `get_file_by_hash` and `get_filename_by_hash` as needed
|
3. Implement `get_file_by_hash` and `get_filename_by_hash`
|
||||||
4. Register in `INDEXER_MAP` dictionary with the file extension as key
|
4. Register in `INDEXER_MAP` dictionary
|
||||||
|
|
||||||
### Modifying the Frontend
|
|
||||||
|
|
||||||
1. Edit `frontend.html` for HTML/CSS/JS changes
|
|
||||||
2. Add new `string.Template` placeholders as needed
|
|
||||||
3. Update `_render_page()` to pass new values
|
|
||||||
4. Ensure navigation URLs are built correctly with `_build_url()`
|
|
||||||
|
|
||||||
### Running the Server in Development
|
### Running the Server in Development
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Direct run (rebuilds on each start)
|
uv run main ./*.zip
|
||||||
uv run main.py /path/to/files
|
|
||||||
|
|
||||||
# With custom options
|
|
||||||
uv run main.py /path/to/files --host 127.0.0.1 --port 3000 --salt mysecret
|
|
||||||
```
|
```
|
||||||
|
|||||||
94
README.md
94
README.md
@ -1,14 +1,13 @@
|
|||||||
# Image Server
|
# Image Server
|
||||||
|
|
||||||
A FastAPI-based image server that serves files from directories or ZIP archives using hashed paths for secure, randomized access. The server provides a full-screen image viewing experience with navigation controls and optional auto-advance features.
|
A FastAPI-based image server that serves files from directories or ZIP archives using hashed paths for secure, randomized access. The server provides a beautiful full-screen image viewing experience with navigation controls and optional auto-advance features.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Secure File Access**: Files are accessed via SHA-256 hashes of their paths (with salt) rather than direct file paths
|
- **Secure File Access**: Files are accessed via SHA-256 hashes of their paths (with salt) rather than direct file paths
|
||||||
- **Multiple Sources**: Supports serving from directories, individual ZIP files, or glob patterns (e.g., `*.zip`)
|
- **Multiple Sources**: Supports serving from directories, individual ZIP files, or glob patterns (e.g., `*.zip`)
|
||||||
- **Password-protected ZIPs**: HTTP Basic Auth for encrypted files inside ZIP archives
|
|
||||||
- **Beautiful UI**: Full-screen responsive image viewer with keyboard navigation
|
- **Beautiful UI**: Full-screen responsive image viewer with keyboard navigation
|
||||||
- **Navigation Controls**:
|
- **Navigation Controls**:
|
||||||
- Previous/Next buttons (clickable chevrons or arrow keys)
|
- Previous/Next buttons (clickable chevrons or arrow keys)
|
||||||
- Random access mode
|
- Random access mode
|
||||||
- Ordered sequential access with configurable delay
|
- Ordered sequential access with configurable delay
|
||||||
@ -27,13 +26,20 @@ A FastAPI-based image server that serves files from directories or ZIP archives
|
|||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
- Python 3.13 or higher
|
- Python 3.13 or higher
|
||||||
- [UV](https://github.com/astral-sh/uv) package manager (recommended) or pip
|
- UV package manager (recommended) or pip
|
||||||
|
|
||||||
### Using UV (Recommended)
|
### Using UV (Recommended)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Clone the repository
|
||||||
|
git clone <repository-url>
|
||||||
cd image_server
|
cd image_server
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
uv sync
|
uv sync
|
||||||
|
|
||||||
|
# Activate virtual environment
|
||||||
|
source .venv/bin/activate
|
||||||
```
|
```
|
||||||
|
|
||||||
### Using pip
|
### Using pip
|
||||||
@ -48,19 +54,19 @@ pip install fastapi uvicorn
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Serve files from a directory
|
# Serve files from a directory
|
||||||
uv run main.py /path/to/your/images
|
python main.py /path/to/your/images
|
||||||
|
|
||||||
# Serve files from a ZIP archive
|
# Serve files from a ZIP archive
|
||||||
uv run main.py /path/to/archive.zip
|
python main.py /path/to/archive.zip
|
||||||
|
|
||||||
# Serve files matching a glob pattern
|
# Serve files matching a glob pattern
|
||||||
uv run main.py "path/to/images/*.zip"
|
python main.py "path/to/images/*.zip"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Server Options
|
### Server Options
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv run main.py [SOURCE] [OPTIONS]
|
python main.py [SOURCE] [OPTIONS]
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
SOURCE Path to directory, ZIP archive, or glob pattern (e.g., *.zip, path/to/zips/*.zip)
|
SOURCE Path to directory, ZIP archive, or glob pattern (e.g., *.zip, path/to/zips/*.zip)
|
||||||
@ -76,41 +82,41 @@ Options:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Start server on default port 8000
|
# Start server on default port 8000
|
||||||
uv run main.py ./photos
|
python main.py ./photos
|
||||||
|
|
||||||
# Start server on custom host and port
|
# Start server on custom host and port
|
||||||
uv run main.py ./photos --host 127.0.0.1 --port 3000
|
python main.py ./photos --host 127.0.0.1 --port 3000
|
||||||
|
|
||||||
# Start server with custom salt for consistent hashing
|
# Start server with custom salt for consistent hashing
|
||||||
uv run main.py ./photos --salt mysecretkey123
|
python main.py ./photos --salt mysecretkey123
|
||||||
|
|
||||||
# Start server serving from ZIP files
|
# Start server serving from ZIP files
|
||||||
uv run main.py ./archives/*.zip
|
python main.py ./archives/*.zip
|
||||||
```
|
```
|
||||||
|
|
||||||
## API Endpoints
|
## API Endpoints
|
||||||
|
|
||||||
### File Access
|
### File Access
|
||||||
|
|
||||||
- `GET /api/{file_hash}/data` — Retrieve file data by hash
|
- `GET /api/{file_hash}/data` - Retrieve file data by hash
|
||||||
- Returns: File content with appropriate Content-Type header
|
- Returns: File content with appropriate Content-Type header
|
||||||
- Response: StreamingResponse of the file data
|
- Response: StreamingResponse of the file data
|
||||||
|
|
||||||
### Navigation Pages
|
### Navigation Pages
|
||||||
|
|
||||||
- `GET /{file_hash}` — View file with navigation controls (browse mode)
|
- `GET /{file_hash}` - View file with navigation controls
|
||||||
- Displays image with previous/next navigation
|
- Displays image with previous/next navigation
|
||||||
|
|
||||||
- `GET /{file_hash}?order=next&delay=5` — View file with auto-refresh
|
- `GET /{order}/{delay}/{file_hash}` - View file with auto-refresh
|
||||||
- `order`: `"next"` (sequential) or `"random"`
|
- order: "next" (sequential) or "random"
|
||||||
- `delay`: Seconds before auto-advancing
|
- delay: Seconds before auto-advancing
|
||||||
- Clicking the image pauses auto-refresh
|
- file_hash: Starting file hash
|
||||||
|
|
||||||
### Special Endpoints
|
### Special Endpoints
|
||||||
|
|
||||||
- `GET /` — Redirect to a random file
|
- `GET /` - Redirect to a random file
|
||||||
- `GET /?order=next&delay=5` — Redirect to random file with order/delay settings
|
- `GET /{order}/{delay}` - Redirect to random file with order/delay settings
|
||||||
- `GET /api/health` — Health check endpoint
|
- `GET /api/health` - Health check endpoint
|
||||||
- Returns: JSON with status and file count
|
- Returns: JSON with status and file count
|
||||||
|
|
||||||
## How It Works
|
## How It Works
|
||||||
@ -132,7 +138,6 @@ Each file is assigned a unique SHA-256 hash based on:
|
|||||||
- Files can only be accessed via their cryptographic hashes
|
- Files can only be accessed via their cryptographic hashes
|
||||||
- Salt prevents hash prediction attacks
|
- Salt prevents hash prediction attacks
|
||||||
- No filesystem traversal vulnerabilities
|
- No filesystem traversal vulnerabilities
|
||||||
- Password-protected ZIP files require HTTP Basic Auth
|
|
||||||
|
|
||||||
### User Interface
|
### User Interface
|
||||||
|
|
||||||
@ -143,8 +148,45 @@ The server serves a single-page application (`frontend.html`) that provides:
|
|||||||
- Play/pause button for auto-advance control
|
- Play/pause button for auto-advance control
|
||||||
- Responsive design that works on mobile and desktop
|
- Responsive design that works on mobile and desktop
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Running in Development Mode
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# With auto-reload for development
|
||||||
|
uvicorn main:app --reload --host 0.0.0.0 --port 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Code Structure
|
||||||
|
|
||||||
|
- `main.py` - Contains all server logic, routing, and file indexing
|
||||||
|
- `frontend.html` - HTML template for the user interface
|
||||||
|
- `FileIndexer` - Base class for indexing files from directories
|
||||||
|
- `ZipFileIndexer` - Extension for indexing files from ZIP archives
|
||||||
|
- Global `file_mapping` dictionary - Maps hashes to file paths/filenames
|
||||||
|
|
||||||
|
### Adding New Features
|
||||||
|
|
||||||
|
1. **New Indexer Types**: Create a class inheriting from `FileIndexer` and override the `_index()` method
|
||||||
|
2. **New Routes**: Add functions decorated with appropriate HTTP method decorators (`@app.get`, etc.)
|
||||||
|
3. **UI Changes**: Modify `frontend.html` template and update template substitution in `_render_page()`
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
- **FastAPI** — Modern web framework for building APIs
|
- **FastAPI** - Modern web framework for building APIs
|
||||||
- **Uvicorn** — ASGI server for running the application
|
- **Uvicorn** - ASGI server for running the application
|
||||||
- **Python Standard Library** — hashlib, mimetypes, secrets, zipfile, etc.
|
- **Python Standard Library** - hashlib, mimetypes, secrets, zipfile, etc.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License - see the LICENSE file for details.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a feature branch
|
||||||
|
3. Commit your changes
|
||||||
|
4. Push to the branch
|
||||||
|
5. Open a pull request
|
||||||
|
|
||||||
|
Please ensure your code follows the existing style and includes appropriate tests.
|
||||||
Loading…
x
Reference in New Issue
Block a user