Browse Source

Initial OTB Cloud scaffold v0.1.0

master
Don Kingdon 3 weeks ago
commit
e8dd3b9f90
  1. 9
      .env.example
  2. 59
      .gitignore
  3. 71
      PROJECT_STATE.md
  4. 64
      README.md
  5. 1
      VERSION
  6. 23
      app/__init__.py
  7. 7
      app/main/routes.py
  8. 1
      app/models/__init__.py
  9. 103
      app/models/schema.sql
  10. 19
      app/templates/cloud/index.html
  11. 3
      requirements.txt
  12. 6
      run.py
  13. 13
      scripts/bootstrap_storage.sh
  14. 30
      scripts/create_tenant_layout.sh

9
.env.example

@ -0,0 +1,9 @@
FLASK_APP=run.py
FLASK_ENV=development
SECRET_KEY=change-me
MARIADB_HOST=127.0.0.1
MARIADB_PORT=3306
MARIADB_DB=otb_cloud
MARIADB_USER=otb_cloud
MARIADB_PASSWORD=change-me
STORAGE_ROOT=/tank/backups/otb-cloud

59
.gitignore vendored

@ -0,0 +1,59 @@
# Python
__pycache__/
*.pyc
*.pyo
*.pyd
.pytest_cache/
.mypy_cache/
.ruff_cache/
.coverage
htmlcov/
# Virtual environments
venv/
.venv/
env/
# Flask / runtime
instance/*.db
instance/*.sqlite
instance/*.sqlite3
*.log
*.pid
tmp/
uploads_tmp/
# Secrets / env
.env
.env.*
!.env.example
# Build artifacts
dist/
build/
*.egg-info/
# Editors / OS
.vscode/
.idea/
*.swp
*~
.DS_Store
Thumbs.db
# Backups / scratch
*.bak
*.old
*.orig
# Generated exports / tarballs
exports/
tarballs/
# Android (future)
android/.gradle/
android/local.properties
android/app/build/
# Node (future)
node_modules/

71
PROJECT_STATE.md

@ -0,0 +1,71 @@
# PROJECT_STATE.md
## Project
OTB Cloud
## Current version
v0.1.0
## Build date
2026-04-12
## Host
vault3
## App path
/opt/otb_cloud
## Purpose
Portal-authenticated secure backup and storage platform for customer files, including images, videos, documents, and other uploaded data.
## Core requirements locked in
- Shared OTB branding, nav, footer, favicon
- Portal login / auth handoff through OTB Billing
- No unauthenticated file/account access
- MariaDB backend
- Vault3 storage root at `/tank/backups/otb-cloud`
- Tenant-isolated storage
- Device-defined source directories
- Immutable originals
- Derived-file processing workflow
- Search by filename and date
- Bulk zip export
- Audit logging
- Owner-approved admin support access using one-time token
## Device organization model
Per-tenant storage will be organized by named devices, for example:
- laptop
- phone
- tablet
- workpc
- homepc
Each device should have:
- originals/
- derived/
- exports/
- deleted/
- tmp/
## Initial app modules planned
- auth
- main
- files
- jobs
- admin
- audit
- services
- models
## Immediate next tasks
1. Add Flask app factory and blueprint registration
2. Add MariaDB config and SQL bootstrap schema
3. Add shared portal template integration
4. Add storage bootstrap script for vault3
5. Add service card integration plan for OTB Billing portal
## Notes
Original uploaded files should remain preserved and effectively read-only.
Any user-facing edits or processing outputs should create derivative files.
Admin access should require owner-issued one-time support authorization.

64
README.md

@ -0,0 +1,64 @@
# OTB Cloud
## v0.1.0 - 2026-04-12
- Initial scaffold created on vault3 at /opt/otb_cloud
- MariaDB-backed architecture selected
- Modular Flask app structure created
- Device-based tenant storage model defined
- Shared OTB portal template architecture planned
- Core project documentation files added
---
## Summary
OTB Cloud is a private portal-authenticated backup and storage platform for Outsidethebox.top.
Primary goals:
- Secure backup and storage for documents, images, videos, and uploaded files
- Per-customer tenant isolation
- Device-based organization (laptop, phone, tablet, workpc, homepc, etc.)
- Immutable original uploads
- Derived file workflow for processing and edits
- Searchable file library
- Bulk upload and bulk export support
- Audit logging
- Owner-approved admin support access using one-time token workflow
## Planned host and path
- Host: vault3
- App path: `/opt/otb_cloud`
- Domain: `otb-cloud.outsidethebox.top`
- Storage root: `/tank/backups/otb-cloud`
## Planned backend stack
- Flask
- MariaDB
- Jinja templates with shared portal base
- Background job processing for media conversions
- FFmpeg for video/audio processing
- Nginx reverse proxy
## Security goals
- Portal-authenticated access only
- No unauthenticated file access
- Tenant-isolated storage and database access
- Audit logging for login attempts and file actions
- Encrypted storage at rest
- HTTPS/TLS in transit
- Immutable originals by default
## Device model
Each tenant may define logical upload devices such as:
- laptop
- phone
- tablet
- workpc
- homepc
Uploads are organized by source device to preserve context.
## Documentation policy
This repository uses:
- `README.md` for version/change log summary
- `PROJECT_STATE.md` for current status and working notes
- `VERSION` for current version

1
VERSION

@ -0,0 +1 @@
v0.1.0

23
app/__init__.py

@ -0,0 +1,23 @@
import os
from flask import Flask
from dotenv import load_dotenv
def create_app():
load_dotenv()
app = Flask(__name__, instance_relative_config=True)
app.config.from_mapping(
SECRET_KEY=os.getenv("SECRET_KEY", "change-me"),
MARIADB_HOST=os.getenv("MARIADB_HOST", "127.0.0.1"),
MARIADB_PORT=int(os.getenv("MARIADB_PORT", "3306")),
MARIADB_DB=os.getenv("MARIADB_DB", "otb_cloud"),
MARIADB_USER=os.getenv("MARIADB_USER", "otb_cloud"),
MARIADB_PASSWORD=os.getenv("MARIADB_PASSWORD", "change-me"),
STORAGE_ROOT=os.getenv("STORAGE_ROOT", "/tank/backups/otb-cloud"),
)
from .main.routes import bp as main_bp
app.register_blueprint(main_bp)
return app

7
app/main/routes.py

@ -0,0 +1,7 @@
from flask import Blueprint, render_template
bp = Blueprint("main", __name__)
@bp.route("/")
def index():
return render_template("cloud/index.html")

1
app/models/__init__.py

@ -0,0 +1 @@
# SQL schema is currently managed in app/models/schema.sql

103
app/models/schema.sql

@ -0,0 +1,103 @@
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
portal_user_id INT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
display_name VARCHAR(255) NULL,
is_active TINYINT(1) NOT NULL DEFAULT 1,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_login_at DATETIME NULL
);
CREATE TABLE IF NOT EXISTS tenants (
id INT AUTO_INCREMENT PRIMARY KEY,
owner_user_id INT NOT NULL,
slug VARCHAR(100) NOT NULL UNIQUE,
storage_root VARCHAR(500) NOT NULL,
service_status VARCHAR(50) NOT NULL DEFAULT 'active',
retention_mode VARCHAR(50) NOT NULL DEFAULT 'standard',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_tenants_owner FOREIGN KEY (owner_user_id) REFERENCES users(id)
);
CREATE TABLE IF NOT EXISTS devices (
id INT AUTO_INCREMENT PRIMARY KEY,
tenant_id INT NOT NULL,
device_name VARCHAR(100) NOT NULL,
device_type VARCHAR(50) NOT NULL,
relative_path VARCHAR(255) NOT NULL,
is_active TINYINT(1) NOT NULL DEFAULT 1,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uq_devices_tenant_name UNIQUE (tenant_id, device_name),
CONSTRAINT fk_devices_tenant FOREIGN KEY (tenant_id) REFERENCES tenants(id)
);
CREATE TABLE IF NOT EXISTS files (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
tenant_id INT NOT NULL,
device_id INT NOT NULL,
parent_file_id BIGINT NULL,
file_kind VARCHAR(20) NOT NULL,
relative_path VARCHAR(1000) NOT NULL,
directory_path VARCHAR(1000) NOT NULL,
original_filename VARCHAR(255) NOT NULL,
basename VARCHAR(255) NOT NULL,
extension VARCHAR(50) NOT NULL,
mime_type VARCHAR(255) NULL,
size_bytes BIGINT NOT NULL DEFAULT 0,
sha256 CHAR(64) NULL,
capture_date DATETIME NULL,
uploaded_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
is_immutable TINYINT(1) NOT NULL DEFAULT 1,
is_deleted TINYINT(1) NOT NULL DEFAULT 0,
deleted_at DATETIME NULL,
CONSTRAINT fk_files_tenant FOREIGN KEY (tenant_id) REFERENCES tenants(id),
CONSTRAINT fk_files_device FOREIGN KEY (device_id) REFERENCES devices(id),
CONSTRAINT fk_files_parent FOREIGN KEY (parent_file_id) REFERENCES files(id)
);
CREATE TABLE IF NOT EXISTS jobs (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
tenant_id INT NOT NULL,
file_id BIGINT NOT NULL,
job_type VARCHAR(100) NOT NULL,
options_json LONGTEXT NULL,
status VARCHAR(50) NOT NULL DEFAULT 'queued',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
started_at DATETIME NULL,
completed_at DATETIME NULL,
output_file_id BIGINT NULL,
log_text LONGTEXT NULL,
CONSTRAINT fk_jobs_tenant FOREIGN KEY (tenant_id) REFERENCES tenants(id),
CONSTRAINT fk_jobs_file FOREIGN KEY (file_id) REFERENCES files(id)
);
CREATE TABLE IF NOT EXISTS audit_logs (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
tenant_id INT NULL,
user_id INT NULL,
actor_type VARCHAR(20) NOT NULL,
event_type VARCHAR(100) NOT NULL,
file_id BIGINT NULL,
job_id BIGINT NULL,
ip_address VARCHAR(64) NULL,
user_agent VARCHAR(500) NULL,
event_detail LONGTEXT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_audit_tenant_created (tenant_id, created_at),
INDEX idx_audit_event_type (event_type)
);
CREATE TABLE IF NOT EXISTS admin_access_tokens (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
tenant_id INT NOT NULL,
issued_by_user_id INT NOT NULL,
used_by_admin_id INT NULL,
token_hash CHAR(64) NOT NULL,
purpose VARCHAR(255) NOT NULL,
status VARCHAR(50) NOT NULL DEFAULT 'issued',
expires_at DATETIME NOT NULL,
used_at DATETIME NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_admin_token_tenant FOREIGN KEY (tenant_id) REFERENCES tenants(id),
CONSTRAINT fk_admin_token_owner FOREIGN KEY (issued_by_user_id) REFERENCES users(id)
);

19
app/templates/cloud/index.html

@ -0,0 +1,19 @@
{% extends "portal_base.html" %}
{% block title %}OTB Cloud{% endblock %}
{% block content %}
<div class="container py-4">
<div class="card shadow-sm rounded-4">
<div class="card-body">
<h1 class="h3 mb-3">OTB Cloud</h1>
<p class="mb-2">
Private backup and storage platform for customer files, device uploads, and media processing.
</p>
<p class="mb-0 text-muted">
Initial scaffold is in place. Portal auth handoff, tenant isolation, searchable library, and processing jobs come next.
</p>
</div>
</div>
</div>
{% endblock %}

3
requirements.txt

@ -0,0 +1,3 @@
Flask==3.1.0
PyMySQL==1.1.1
python-dotenv==1.0.1

6
run.py

@ -0,0 +1,6 @@
from app import create_app
app = create_app()
if __name__ == "__main__":
app.run(host="127.0.0.1", port=5090, debug=True)

13
scripts/bootstrap_storage.sh

@ -0,0 +1,13 @@
#!/usr/bin/env bash
set -e
BASE="/tank/backups/otb-cloud"
echo "===== ensuring base storage root exists ====="
sudo mkdir -p "$BASE/tenants"
sudo mkdir -p "$BASE/system/job-output"
sudo mkdir -p "$BASE/system/logs"
sudo mkdir -p "$BASE/system/quarantine"
echo "===== base storage root prepared ====="
echo "===== script completed successfully ====="

30
scripts/create_tenant_layout.sh

@ -0,0 +1,30 @@
#!/usr/bin/env bash
set -e
if [ $# -lt 2 ]; then
echo "usage: $0 <tenant_slug> <device_name> [device_name ...]"
exit 1
fi
BASE="/tank/backups/otb-cloud/tenants"
TENANT="$1"
shift
TENANT_ROOT="$BASE/$TENANT"
echo "===== creating tenant root for $TENANT ====="
sudo mkdir -p "$TENANT_ROOT"
for DEVICE in "$@"; do
sudo mkdir -p "$TENANT_ROOT/devices/$DEVICE/originals"
sudo mkdir -p "$TENANT_ROOT/devices/$DEVICE/derived"
sudo mkdir -p "$TENANT_ROOT/devices/$DEVICE/exports"
sudo mkdir -p "$TENANT_ROOT/devices/$DEVICE/deleted"
sudo mkdir -p "$TENANT_ROOT/devices/$DEVICE/tmp"
done
sudo mkdir -p "$TENANT_ROOT/logs"
sudo mkdir -p "$TENANT_ROOT/support"
echo "===== tenant layout created ====="
echo "===== script completed successfully ====="
Loading…
Cancel
Save