This commit is contained in:
retoor 2025-11-09 14:13:06 +01:00
parent 925f91a17c
commit d8a419f528
21 changed files with 1115 additions and 345 deletions

View File

@ -1,4 +1,6 @@
import aiosmtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.message import EmailMessage
from aiohttp import web
import logging
@ -24,11 +26,12 @@ async def send_email(app: web.Application, recipient_email: str, subject: str, b
logger.error("SMTP host or sender email not configured. Cannot send email.")
return
msg = EmailMessage()
msg = MIMEMultipart('alternative')
msg["From"] = smtp_sender_email
msg["To"] = recipient_email
msg["Subject"] = subject
msg.set_content(body)
html_part = MIMEText(body, 'html')
msg.attach(html_part)
try:
await aiosmtplib.send(

View File

@ -1,3 +1,5 @@
import argparse
from aiohttp import web
import aiohttp_jinja2
import jinja2
@ -20,7 +22,7 @@ async def setup_services(app: web.Application):
data_path = base_path.parent / "data"
app["user_service"] = UserService(use_isolated_storage=True)
app["config_service"] = ConfigService(data_path / "config.json")
app["file_service"] = FileService(data_path / "user_files", data_path / "users.json")
app["file_service"] = FileService(data_path / "user_files", app["user_service"])
# Setup aiojobs scheduler
app["scheduler"] = aiojobs.Scheduler()
@ -67,9 +69,15 @@ def create_app():
return app
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--port', type=int, default=8080)
parser.add_argument('--hostname', default='0.0.0.0')
args = parser.parse_args()
app = create_app()
web.run_app(app)
web.run_app(app, host=args.hostname, port=args.port)
if __name__ == "__main__":

View File

@ -3,7 +3,8 @@ from .views.site import SiteView, OrderView, FileBrowserView, UserManagementView
from .views.upload import UploadView
from .views.migrate import MigrateView
from .views.admin import get_users, add_user, update_user_quota, delete_user, get_user_details, delete_team
from .views.editor import FileEditorView, FileContentView
from .views.viewer import ViewerView
def setup_routes(app):
app.router.add_view("/login", LoginView, name="login")
@ -21,6 +22,12 @@ def setup_routes(app):
app.router.add_view("/order", OrderView, name="order")
app.router.add_view("/terms", SiteView, name="terms")
app.router.add_view("/privacy", SiteView, name="privacy")
app.router.add_view("/cookies", SiteView, name="cookies")
app.router.add_view("/impressum", SiteView, name="impressum")
app.router.add_view("/user_rights", SiteView, name="user_rights")
app.router.add_view("/aup", SiteView, name="aup")
app.router.add_view("/sla", SiteView, name="sla")
app.router.add_view("/compliance", SiteView, name="compliance")
app.router.add_view("/shared", SiteView, name="shared")
app.router.add_view("/recent", SiteView, name="recent")
app.router.add_view("/favorites", SiteView, name="favorites")
@ -40,6 +47,10 @@ def setup_routes(app):
app.router.add_post("/files/share_multiple", FileBrowserView, name="share_multiple_items")
app.router.add_get("/shared_file/{share_id}", FileBrowserView.shared_file_handler, name="shared_file")
app.router.add_get("/shared_file/{share_id}/download", FileBrowserView.download_shared_file_handler, name="download_shared_file")
app.router.add_view("/editor", FileEditorView, name="file_editor")
app.router.add_view("/viewer", ViewerView, name="file_viewer")
app.router.add_get("/api/file/content", FileContentView, name="get_file_content")
app.router.add_post("/api/file/save", FileEditorView, name="save_file_content")
# Admin API routes for user and team management
app.router.add_get("/api/users", get_users, name="api_get_users")

View File

@ -137,6 +137,63 @@ class FileService:
logger.info(f"download_file: Successfully read file: {file_path}")
return content, Path(file_path).name
async def read_file_content_binary(self, user_email: str, file_path: str) -> str | None:
"""Reads file content as text for editing."""
metadata = await self._load_metadata(user_email)
if file_path not in metadata or metadata[file_path]["type"] != "file":
logger.warning(f"read_file_content: File not found in metadata: {file_path}")
return None
item_meta = metadata[file_path]
blob_loc = item_meta["blob_location"]
blob_path = self.drives_dir / blob_loc["drive"] / blob_loc["path"]
if not blob_path.exists():
logger.warning(f"read_file_content: Blob not found: {blob_path}")
return None
try:
async with aiofiles.open(blob_path, 'rb') as f:
content = await f.read()
logger.info(f"read_file_content: Successfully read file: {file_path}")
return content
except UnicodeDecodeError:
logger.warning(f"read_file_content: File is not a text file: {file_path}")
return None
async def read_file_content(self, user_email: str, file_path: str) -> str | None:
"""Reads file content as text for editing."""
metadata = await self._load_metadata(user_email)
if file_path not in metadata or metadata[file_path]["type"] != "file":
logger.warning(f"read_file_content: File not found in metadata: {file_path}")
return None
item_meta = metadata[file_path]
blob_loc = item_meta["blob_location"]
blob_path = self.drives_dir / blob_loc["drive"] / blob_loc["path"]
if not blob_path.exists():
logger.warning(f"read_file_content: Blob not found: {blob_path}")
return None
try:
async with aiofiles.open(blob_path, 'r', encoding='utf-8') as f:
content = await f.read()
logger.info(f"read_file_content: Successfully read file: {file_path}")
return content
except UnicodeDecodeError:
logger.warning(f"read_file_content: File is not a text file: {file_path}")
return None
async def save_file_content(self, user_email: str, file_path: str, content: str) -> bool:
"""Saves file content from editor."""
try:
content_bytes = content.encode('utf-8')
success = await self.upload_file(user_email, file_path, content_bytes)
if success:
logger.info(f"save_file_content: Successfully saved file: {file_path}")
return success
except Exception as e:
logger.error(f"save_file_content: Error saving file {file_path}: {e}")
return False
async def delete_item(self, user_email: str, item_path: str) -> bool:
"""Deletes a file or folder for the user."""
metadata = await self._load_metadata(user_email)

View File

@ -4,6 +4,7 @@ import hashlib
import aiofiles
from pathlib import Path
from typing import Any, Dict, List, Optional
from .lock_manager import get_lock_manager
class StorageService:
@ -11,6 +12,7 @@ class StorageService:
def __init__(self, base_path: str = "data/user"):
self.base_path = Path(base_path)
self.base_path.mkdir(parents=True, exist_ok=True)
self.lock_manager = get_lock_manager()
def _hash(self, value: str) -> str:
return hashlib.sha256(value.encode()).hexdigest()
@ -43,8 +45,10 @@ class StorageService:
file_path.parent.mkdir(parents=True, exist_ok=True)
async with aiofiles.open(file_path, 'w') as f:
await f.write(json.dumps(data, indent=2))
lock = await self.lock_manager.get_lock(str(file_path))
async with lock:
async with aiofiles.open(file_path, 'w') as f:
await f.write(json.dumps(data, indent=2))
return True
@ -58,14 +62,16 @@ class StorageService:
if not file_path.exists():
return None
async with aiofiles.open(file_path, 'r') as f:
content = await f.read()
if not content:
return {}
try:
return json.loads(content)
except json.JSONDecodeError:
return {}
lock = await self.lock_manager.get_lock(str(file_path))
async with lock:
async with aiofiles.open(file_path, 'r') as f:
content = await f.read()
if not content:
return {}
try:
return json.loads(content)
except json.JSONDecodeError:
return {}
async def delete(self, user_email: str, identifier: str) -> bool:
user_base = self._get_user_base_path(user_email)
@ -75,8 +81,11 @@ class StorageService:
raise ValueError("Invalid path: directory traversal detected")
if file_path.exists():
file_path.unlink()
return True
lock = await self.lock_manager.get_lock(str(file_path))
async with lock:
if file_path.exists():
file_path.unlink()
return True
return False
@ -98,9 +107,11 @@ class StorageService:
results = []
for json_file in user_base.rglob("*.json"):
if self._validate_path(json_file, user_base):
async with aiofiles.open(json_file, 'r') as f:
content = await f.read()
results.append(json.loads(content))
lock = await self.lock_manager.get_lock(str(json_file))
async with lock:
async with aiofiles.open(json_file, 'r') as f:
content = await f.read()
results.append(json.loads(content))
return results
@ -122,6 +133,7 @@ class UserStorageManager:
def __init__(self):
self.storage = StorageService()
self.lock_manager = get_lock_manager()
async def save_user(self, user_email: str, user_data: Dict[str, Any]) -> bool:
return await self.storage.save(user_email, user_email, user_data)
@ -146,11 +158,13 @@ class UserStorageManager:
if user_dir.is_dir():
user_files = list(user_dir.rglob("*.json"))
for user_file in user_files:
async with aiofiles.open(user_file, 'r') as f:
content = await f.read()
user_data = json.loads(content)
if user_data.get('parent_email') == parent_email:
all_users.append(user_data)
lock = await self.lock_manager.get_lock(str(user_file))
async with lock:
async with aiofiles.open(user_file, 'r') as f:
content = await f.read()
user_data = json.loads(content)
if user_data.get('parent_email') == parent_email:
all_users.append(user_data)
return all_users
@ -165,8 +179,10 @@ class UserStorageManager:
if user_dir.is_dir():
user_files = list(user_dir.rglob("*.json"))
for user_file in user_files:
async with aiofiles.open(user_file, 'r') as f:
content = await f.read()
all_users.append(json.loads(content))
lock = await self.lock_manager.get_lock(str(user_file))
async with lock:
async with aiofiles.open(user_file, 'r') as f:
content = await f.read()
all_users.append(json.loads(content))
return all_users

View File

@ -5,11 +5,13 @@ import bcrypt
import secrets
import datetime
from .storage_service import UserStorageManager
from .lock_manager import get_lock_manager
class UserService:
def __init__(self, users_path: Path = None, use_isolated_storage: bool = True):
self.use_isolated_storage = use_isolated_storage
self.lock_manager = get_lock_manager()
if use_isolated_storage:
self._storage_manager = UserStorageManager()
@ -27,11 +29,13 @@ class UserService:
except json.JSONDecodeError:
return []
def _save_users(self):
async def _save_users(self):
if self.use_isolated_storage:
return
with open(self._users_path, "w") as f:
json.dump(self._users, f, indent=4)
lock = await self.lock_manager.get_lock(str(self._users_path))
async with lock:
with open(self._users_path, "w") as f:
json.dump(self._users, f, indent=4)
async def get_user_by_email(self, email: str) -> Optional[Dict[str, Any]]:
if self.use_isolated_storage:
@ -80,7 +84,7 @@ class UserService:
await self._storage_manager.save_user(email, user)
else:
self._users.append(user)
self._save_users()
await self._save_users()
return user
@ -98,7 +102,7 @@ class UserService:
if self.use_isolated_storage:
await self._storage_manager.save_user(email, user)
else:
self._save_users()
await self._save_users()
return user
@ -109,7 +113,7 @@ class UserService:
initial_len = len(self._users)
self._users = [user for user in self._users if user["email"] != email]
if len(self._users) < initial_len:
self._save_users()
await self._save_users()
return True
return False
@ -126,7 +130,7 @@ class UserService:
self._users = [user for user in self._users if user.get("parent_email") != parent_email]
deleted_count = initial_len - len(self._users)
if deleted_count > 0:
self._save_users()
await self._save_users()
return deleted_count
@ -160,7 +164,7 @@ class UserService:
if self.use_isolated_storage:
await self._storage_manager.save_user(email, user)
else:
self._save_users()
await self._save_users()
return token
@ -192,7 +196,7 @@ class UserService:
if self.use_isolated_storage:
await self._storage_manager.save_user(email, user)
else:
self._save_users()
await self._save_users()
return True
@ -206,4 +210,4 @@ class UserService:
if self.use_isolated_storage:
await self._storage_manager.save_user(email, user)
else:
self._save_users()
await self._save_users()

View File

@ -1,64 +1,72 @@
:root {
--primary-color: #4A90E2; /* Blue from image */
--accent-color: #50E3C2; /* Greenish-blue from image */
--secondary-color: #B8C2CC; /* Light grey-blue */
--background-color: #F8F8F8; /* Very light grey background */
--text-color: #333333; /* Darker text */
--light-text-color: #666666; /* Lighter text for descriptions */
--border-color: #E0E0E0; /* Light grey border */
--primary-color: #0066FF; /* Vibrant blue */
--accent-color: #00D4FF; /* Bright cyan accent */
--secondary-color: #F7FAFC; /* Very light blue-grey */
--background-color: #FFFFFF; /* Pure white background */
--text-color: #1A202C; /* Dark blue-grey */
--light-text-color: #718096; /* Medium grey for descriptions */
--border-color: #E2E8F0; /* Light grey border */
--card-background: #FFFFFF; /* White for cards */
--shadow-color: rgba(0, 0, 0, 0.08); /* Subtle shadow */
--shadow-color: rgba(0, 0, 0, 0.05); /* Very subtle shadow */
/* Button specific variables, using accent for primary CTAs */
/* Button specific variables */
--btn-primary-bg: var(--primary-color);
--btn-primary-text: #FFFFFF;
--btn-primary-hover-bg: #3A7BD5; /* Slightly darker blue */
--btn-secondary-bg: #E0E0E0; /* Light grey for secondary buttons */
--btn-primary-hover-bg: #0052CC; /* Darker blue */
--btn-secondary-bg: #EDF2F7; /* Light grey */
--btn-secondary-text: var(--text-color);
--btn-secondary-hover-bg: #BDBDBD; /* Darker grey for secondary hover */
--btn-secondary-hover-bg: #E2E8F0; /* Darker grey */
--btn-outline-border: var(--primary-color);
--btn-outline-text: var(--primary-color);
--btn-outline-hover-bg: rgba(74, 144, 226, 0.1); /* Light blue hover */
--btn-outline-hover-bg: rgba(0, 102, 255, 0.05); /* Very light blue hover */
}
html, body {
height: 100%;
margin: 0;
font-family: 'Roboto', sans-serif; /* Using a more modern font */
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
background-color: var(--background-color);
color: var(--text-color);
display: flex;
flex-direction: column;
line-height: 1.6; /* Improve readability */
line-height: 1.6;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-size: 16px;
}
/* General typography */
h1 {
font-size: 2.2rem; /* Slightly larger heading */
font-size: 3.5rem;
font-weight: 700;
margin-bottom: 1rem;
margin-bottom: 1.5rem;
color: var(--text-color);
line-height: 1.2;
letter-spacing: -0.02em;
}
h2 {
font-size: 2.2rem;
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 0.8rem;
margin-bottom: 1rem;
color: var(--text-color);
line-height: 1.3;
letter-spacing: -0.01em;
}
h3 {
font-size: 1.8rem;
font-weight: 500;
margin-bottom: 0.6rem;
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 0.75rem;
color: var(--text-color);
line-height: 1.4;
}
p {
margin-bottom: 1rem;
color: var(--light-text-color);
font-size: 1.125rem;
line-height: 1.7;
}
/* Card-like styling for sections */
@ -147,96 +155,100 @@ p {
}
/* Header and Navigation */
header {
background-color: var(--card-background); /* White background for header */
box-shadow: 0 2px 4px var(--shadow-color);
padding: 15px 20px;
.site-header {
background-color: #FFFFFF;
border-bottom: 1px solid var(--border-color);
position: sticky;
top: 0;
z-index: 1000;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
nav {
.site-nav {
padding: 0 20px;
}
.nav-container {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
max-width: 1200px;
margin: 0 auto;
min-height: 70px;
gap: 2rem;
}
nav .logo {
.brand {
text-decoration: none;
display: flex;
align-items: center;
text-decoration: none;
color: var(--text-color);
flex-shrink: 0;
}
.brand-text {
font-size: 1.5rem;
font-weight: 700;
color: var(--text-color);
letter-spacing: -0.01em;
}
nav .logo img {
height: 30px; /* Adjust logo size */
margin-right: 10px;
}
nav .nav-links {
.nav-menu {
display: flex;
flex-direction: row;
list-style: none;
margin: 0;
padding: 0;
gap: 30px;
gap: 2rem;
align-items: center;
}
nav .nav-links a {
.nav-menu li {
list-style: none;
}
.nav-menu a {
color: var(--text-color);
text-decoration: none;
font-weight: 500;
transition: color 0.3s ease;
font-size: 1rem;
transition: color 0.2s ease;
white-space: nowrap;
}
nav .nav-links a:hover {
.nav-menu a:hover {
color: var(--primary-color);
}
.btn-primary-nav {
background-color: var(--primary-color);
color: white;
padding: 10px 20px;
border-radius: 5px;
.nav-actions {
display: flex;
flex-direction: row;
align-items: center;
gap: 1rem;
flex-shrink: 0;
}
.nav-link {
color: var(--text-color);
text-decoration: none;
font-weight: 500;
transition: background-color 0.3s ease;
font-size: 1rem;
transition: color 0.2s ease;
white-space: nowrap;
}
.btn-primary-nav:hover {
background-color: var(--btn-primary-hover-bg);
}
.btn-outline-nav {
background-color: transparent;
.nav-link:hover {
color: var(--primary-color);
border: 1px solid var(--primary-color);
padding: 10px 20px;
border-radius: 5px;
text-decoration: none;
font-weight: 500;
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
}
.btn-outline-nav:hover {
background-color: rgba(74, 144, 226, 0.1);
color: var(--primary-color);
border-color: var(--primary-color);
}
/* Main content area */
main {
flex-grow: 1; /* Allow main content to take available space */
flex-grow: 1;
width: 100%;
max-width: 1200px; /* Match header max-width */
margin: 0 auto; /* Center the main content */
padding: 40px 20px; /* Add padding */
margin: 0;
padding: 0;
box-sizing: border-box;
background-color: var(--background-color); /* Light background for main content */
background-color: var(--background-color);
}
/* Form specific styles */
@ -244,17 +256,17 @@ main {
max-width: 450px;
margin: 50px auto;
background-color: var(--card-background);
border-radius: 12px; /* Slightly more rounded corners */
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1); /* Softer, more prominent shadow */
padding: 3rem; /* Increased padding */
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
padding: 2.5rem;
text-align: left;
}
.form-container h2 {
text-align: center;
margin-bottom: 35px; /* Increased margin */
margin-bottom: 2rem;
color: var(--text-color);
font-size: 2.2rem; /* Slightly larger heading */
font-size: 2rem;
font-weight: 700;
}
@ -309,16 +321,17 @@ main {
.btn-small,
.btn-danger {
display: inline-block;
padding: 0.6rem 1.2rem;
font-size: 0.9rem;
font-weight: 500;
padding: 0.65rem 1.5rem;
font-size: 0.95rem;
font-weight: 600;
text-align: center;
text-decoration: none;
border-radius: 4px;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
border: 1px solid transparent;
border: 2px solid transparent;
box-sizing: border-box;
white-space: nowrap;
}
.btn-primary {
@ -330,6 +343,8 @@ main {
.btn-primary:hover {
background-color: var(--btn-primary-hover-bg);
border-color: var(--btn-primary-hover-bg);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 102, 255, 0.2);
}
.btn-outline {
@ -342,6 +357,7 @@ main {
background-color: var(--btn-outline-hover-bg);
color: var(--btn-outline-text);
border-color: var(--btn-outline-border);
transform: translateY(-1px);
}
.btn-small {
@ -372,28 +388,81 @@ main {
text-align: center;
}
/* Responsive adjustments for base.css */
@media (max-width: 768px) {
nav {
flex-wrap: wrap;
justify-content: center;
gap: 15px;
/* Responsive adjustments */
@media (max-width: 992px) {
.nav-menu {
gap: 1.25rem;
}
nav .logo {
width: 100%;
justify-content: center;
margin-bottom: 10px;
}
nav .nav-links {
width: 100%;
justify-content: center;
margin-bottom: 10px;
}
.btn-primary-nav {
width: 100%;
text-align: center;
}
main {
padding: 20px 15px;
.nav-container {
gap: 1rem;
}
}
@media (max-width: 768px) {
.site-nav {
padding: 10px 15px;
}
.nav-container {
flex-wrap: wrap;
min-height: auto;
padding: 10px 0;
gap: 1rem;
}
.brand {
flex: 1 1 100%;
justify-content: center;
order: 1;
margin-bottom: 10px;
}
.nav-menu {
flex: 1 1 100%;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
gap: 1rem;
order: 2;
margin-bottom: 10px;
}
.nav-menu a {
font-size: 0.9rem;
}
.nav-actions {
flex: 1 1 100%;
justify-content: center;
order: 3;
gap: 0.75rem;
}
}
@media (max-width: 480px) {
.brand-text {
font-size: 1.25rem;
}
.nav-menu {
gap: 0.75rem;
}
.nav-menu a {
font-size: 0.85rem;
}
.nav-actions {
flex-direction: column;
gap: 0.5rem;
width: 100%;
}
.nav-actions .btn-primary,
.nav-actions .btn-outline {
width: 100%;
max-width: 280px;
text-align: center;
}
}

View File

@ -1,3 +1,72 @@
/* Content pages main wrapper */
main.content-page {
max-width: 1200px;
margin: 0 auto;
padding: 40px 20px;
}
/* Content sections for legal and information pages */
.content-section {
max-width: 900px;
margin: 0 auto;
padding: 60px 20px;
}
.content-section h1 {
font-size: 2.5rem;
margin-bottom: 1.5rem;
color: var(--text-color);
}
.content-section h2 {
font-size: 1.75rem;
margin-top: 2.5rem;
margin-bottom: 1rem;
color: var(--text-color);
}
.content-section h3 {
font-size: 1.25rem;
margin-top: 2rem;
margin-bottom: 0.75rem;
color: var(--text-color);
}
.content-section p {
margin-bottom: 1.25rem;
line-height: 1.7;
}
.content-section ul {
margin-bottom: 1.5rem;
padding-left: 2rem;
}
.content-section li {
margin-bottom: 0.5rem;
line-height: 1.6;
}
.content-section table {
width: 100%;
border-collapse: collapse;
margin: 2rem 0;
background-color: var(--card-background);
}
.content-section table th,
.content-section table td {
padding: 1rem;
border: 1px solid var(--border-color);
text-align: left;
}
.content-section table th {
background-color: var(--secondary-color);
font-weight: 600;
color: var(--text-color);
}
/* General styles for hero sections on content pages */
.hero-intro {
padding: 20px;

View File

@ -10,15 +10,25 @@
}
.dashboard-sidebar {
flex: 0 0 250px; /* Fixed width sidebar */
flex: 0 0 250px;
background-color: var(--card-background);
border-radius: 8px;
box-shadow: 0 4px 12px var(--shadow-color);
padding: 20px;
position: sticky; /* Make sidebar sticky */
top: 100px; /* Adjust based on header height */
max-height: calc(100vh - 120px); /* Adjust based on header/footer */
overflow-y: auto;
position: sticky;
top: 100px;
max-height: calc(100vh - 120px);
overflow-y: hidden;
overflow-x: hidden;
}
.dashboard-sidebar::-webkit-scrollbar {
display: none;
}
.dashboard-sidebar {
-ms-overflow-style: none;
scrollbar-width: none;
}
.sidebar-menu ul {
@ -162,6 +172,7 @@
width: 100%;
border-collapse: collapse;
margin-top: 20px;
table-layout: fixed;
}
.file-list-table th,
@ -181,6 +192,9 @@
.file-list-table td {
color: var(--light-text-color);
word-break: break-all;
overflow-wrap: break-word;
max-width: 300px;
}
.file-list-table tr:last-child td {
@ -200,6 +214,43 @@
color: var(--light-text-color);
}
.file-list-table a {
word-break: break-all;
overflow-wrap: break-word;
}
.file-list-table th:first-child,
.file-list-table td:first-child {
width: 40px;
max-width: 40px;
}
.file-list-table th:nth-child(2),
.file-list-table td:nth-child(2) {
width: 40%;
min-width: 150px;
}
.file-list-table th:nth-child(3),
.file-list-table td:nth-child(3) {
width: 20%;
}
.file-list-table th:nth-child(4),
.file-list-table td:nth-child(4) {
width: 15%;
}
.file-list-table th:nth-child(5),
.file-list-table td:nth-child(5) {
width: 10%;
}
.file-list-table th:last-child,
.file-list-table td:last-child {
width: 15%;
}
/* Responsive adjustments */
@media (max-width: 992px) {
.dashboard-layout {
@ -249,4 +300,8 @@
padding: 10px 12px;
font-size: 0.85rem;
}
.file-list-table td {
max-width: 150px;
}
}

View File

@ -1,5 +1,20 @@
.breadcrumb {
margin-bottom: 20px;
font-size: 0.9rem;
color: var(--light-text-color);
}
.breadcrumb a {
color: var(--accent-color);
text-decoration: none;
}
.breadcrumb a:hover {
text-decoration: underline;
}
.modal {
display: none;
display: none !important;
position: fixed;
z-index: 1000;
left: 0;
@ -10,6 +25,10 @@
background-color: rgba(0, 0, 0, 0.5);
}
.modal.show {
display: block !important;
}
.modal-content {
background-color: var(--card-background);
margin: 10% auto;
@ -161,6 +180,7 @@
.file-list-table table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
.file-list-table th {
@ -177,6 +197,9 @@
padding: 12px 15px;
border-bottom: 1px solid var(--border-color);
color: var(--text-color);
word-break: break-all;
overflow-wrap: break-word;
max-width: 300px;
}
.file-list-table tr:hover {
@ -197,12 +220,46 @@
.file-list-table a {
color: var(--accent-color);
text-decoration: none;
word-break: break-all;
overflow-wrap: break-word;
}
.file-list-table a:hover {
text-decoration: underline;
}
.file-list-table th:first-child,
.file-list-table td:first-child {
width: 40px;
max-width: 40px;
}
.file-list-table th:nth-child(2),
.file-list-table td:nth-child(2) {
width: 40%;
min-width: 150px;
}
.file-list-table th:nth-child(3),
.file-list-table td:nth-child(3) {
width: 20%;
}
.file-list-table th:nth-child(4),
.file-list-table td:nth-child(4) {
width: 15%;
}
.file-list-table th:nth-child(5),
.file-list-table td:nth-child(5) {
width: 10%;
}
.file-list-table th:last-child,
.file-list-table td:last-child {
width: 15%;
}
@media (max-width: 768px) {
.modal-content {
width: 95%;
@ -227,6 +284,10 @@
padding: 8px 10px;
}
.file-list-table td {
max-width: 150px;
}
.dashboard-actions {
flex-wrap: wrap;
}

View File

@ -1,14 +1,70 @@
footer {
background-color: var(--card-background); /* Consistent with header */
color: var(--light-text-color); /* Subtle text color */
padding: 1.5rem 2rem;
text-align: center;
border-top: 1px solid var(--border-color); /* Separator from content */
margin-top: auto; /* Push footer to the bottom */
box-shadow: 0 -2px 4px var(--shadow-color); /* Subtle shadow upwards */
background-color: var(--card-background);
color: var(--light-text-color);
padding: 2rem 2rem 1rem 2rem;
border-top: 1px solid var(--border-color);
margin-top: auto;
box-shadow: 0 -2px 4px var(--shadow-color);
}
footer p {
margin: 0; /* Remove default paragraph margin */
font-size: 0.9rem;
.footer-content {
display: flex;
justify-content: space-around;
max-width: 1200px;
margin: 0 auto 2rem auto;
gap: 2rem;
}
.footer-section {
flex: 1;
}
.footer-section h4 {
color: var(--text-color);
font-size: 0.95rem;
margin: 0 0 1rem 0;
font-weight: 600;
}
.footer-section ul {
list-style: none;
padding: 0;
margin: 0;
}
.footer-section ul li {
margin-bottom: 0.5rem;
}
.footer-section ul li a {
color: var(--light-text-color);
text-decoration: none;
font-size: 0.85rem;
transition: color 0.2s ease;
}
.footer-section ul li a:hover {
color: var(--accent-color);
}
.footer-bottom {
text-align: center;
padding-top: 1rem;
border-top: 1px solid var(--border-color);
}
.footer-bottom p {
margin: 0;
font-size: 0.85rem;
}
@media (max-width: 768px) {
.footer-content {
flex-direction: column;
gap: 1.5rem;
}
.footer-section {
text-align: center;
}
}

View File

@ -1,135 +1,260 @@
/* Styles for the Homepage (index.html) */
/* Hero Section */
.hero-section {
text-align: center;
padding: 60px 20px;
/* Removed background-color and box-shadow to match image */
margin-bottom: 40px;
position: relative;
overflow: hidden;
padding: 100px 20px 80px;
background: linear-gradient(135deg, #F7FAFC 0%, #FFFFFF 100%);
margin-bottom: 0;
}
.hero-content {
max-width: 900px;
margin: 0 auto;
}
.hero-section h1 {
font-size: 3.5rem;
font-size: 3.75rem;
font-weight: 700;
color: var(--text-color);
margin-bottom: 0.5rem; /* Adjusted margin to match image */
line-height: 1.2;
margin-bottom: 1.5rem;
line-height: 1.15;
letter-spacing: -0.03em;
}
.hero-section p {
font-size: 1.3rem;
.hero-subtitle {
font-size: 1.25rem;
color: var(--light-text-color);
max-width: 800px;
margin: 0 auto 2.5rem auto;
line-height: 1.5;
max-width: 700px;
margin: 0 auto 2.5rem;
line-height: 1.6;
}
.benefits-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 30px;
max-width: 1200px;
margin: 40px auto; /* Adjusted margin to separate from text */
}
.benefit-card {
background-color: var(--card-background);
padding: 30px;
border-radius: 8px;
box-shadow: 0 4px 15px var(--shadow-color);
text-align: center;
transition: transform 0.3s ease-in-out, box-shadow 0.3s ease-in-out;
.hero-ctas {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
justify-content: center;
min-height: 250px; /* Ensure cards have a consistent height */
margin-bottom: 1.5rem;
}
.benefit-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.12);
.hero-btn {
padding: 1rem 2rem;
font-size: 1.125rem;
font-weight: 600;
border-radius: 8px;
text-decoration: none;
transition: all 0.3s ease;
}
.benefit-card img.icon {
width: 60px;
height: 60px;
margin-bottom: 20px;
/* Icons in the image are colored, not inheriting text color */
.hero-subtext {
font-size: 0.875rem;
color: var(--light-text-color);
margin: 0;
}
.benefit-card h3 {
font-size: 1.5rem;
/* Features Section */
.features-section {
padding: 80px 20px;
background-color: #FFFFFF;
}
.features-header {
text-align: center;
max-width: 700px;
margin: 0 auto 60px;
}
.features-header h2 {
font-size: 2.5rem;
margin-bottom: 1rem;
}
.features-header p {
font-size: 1.125rem;
color: var(--light-text-color);
}
.features-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 2.5rem;
max-width: 1200px;
margin: 0 auto;
}
.feature-card {
text-align: center;
padding: 2rem;
}
.feature-icon {
color: var(--primary-color);
margin-bottom: 1.5rem;
display: inline-block;
}
.feature-card h3 {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 0.75rem;
color: var(--text-color);
margin-bottom: 15px;
}
.benefit-card p {
.feature-card p {
font-size: 1rem;
color: var(--light-text-color);
line-height: 1.6;
}
/* Specific card colors from the image */
.family-card {
background-color: #D0E6F0; /* Light blue */
/* Use Cases Section */
.use-cases-section {
padding: 80px 20px;
background: linear-gradient(135deg, #F7FAFC 0%, #EDF2F7 100%);
text-align: center;
}
.professional-card {
background-color: #F0E0D0; /* Light orange */
.use-cases-section h2 {
font-size: 2.5rem;
margin-bottom: 3rem;
color: var(--text-color);
}
.student-card {
background-color: #D0F0D0; /* Light green */
.use-cases-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
max-width: 1200px;
margin: 0 auto;
}
.find-plan-btn {
margin-top: 40px;
padding: 15px 30px;
font-size: 1.1rem;
.use-case-card {
background-color: #FFFFFF;
padding: 2.5rem;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.use-case-card:hover {
transform: translateY(-8px);
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
}
.use-case-icon {
width: 64px;
height: 64px;
margin-bottom: 1.5rem;
}
.use-case-card h3 {
font-size: 1.5rem;
font-weight: 600;
border-radius: 5px;
background-color: #4A90E2; /* Blue from the image */
margin-bottom: 1rem;
color: var(--text-color);
}
.use-case-card p {
font-size: 1rem;
color: var(--light-text-color);
line-height: 1.6;
}
/* CTA Section */
.cta-section {
padding: 100px 20px;
background: linear-gradient(135deg, var(--primary-color) 0%, #0052CC 100%);
text-align: center;
color: white;
}
.cta-content {
max-width: 700px;
margin: 0 auto;
}
.cta-section h2 {
font-size: 2.75rem;
color: white;
margin-bottom: 1rem;
}
.cta-section p {
font-size: 1.25rem;
color: rgba(255, 255, 255, 0.9);
margin-bottom: 2.5rem;
}
.cta-btn {
padding: 1rem 2.5rem;
font-size: 1.125rem;
font-weight: 600;
background-color: white;
color: var(--primary-color);
border-radius: 8px;
text-decoration: none;
display: inline-block;
transition: background-color 0.3s ease;
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.find-plan-btn:hover {
background-color: #3A7BD5; /* Darker blue on hover */
.cta-btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2);
}
/* Responsive adjustments for index.css */
/* Responsive Design */
@media (max-width: 992px) {
.hero-section h1 {
font-size: 3rem;
}
.hero-section p {
font-size: 1.2rem;
.features-grid {
grid-template-columns: repeat(2, 1fr);
}
.features-header h2,
.use-cases-section h2,
.cta-section h2 {
font-size: 2rem;
}
}
@media (max-width: 768px) {
.hero-section {
padding: 40px 15px;
padding: 60px 20px 50px;
}
.hero-section h1 {
font-size: 2.5rem;
}
.hero-section p {
font-size: 1rem;
.hero-subtitle {
font-size: 1.125rem;
}
.benefits-grid {
grid-template-columns: 1fr; /* Stack cards on small screens */
.hero-ctas {
flex-direction: column;
gap: 0.75rem;
}
.benefit-card {
padding: 25px;
}
.find-plan-btn {
.hero-btn {
width: 100%;
box-sizing: border-box;
max-width: 300px;
}
.features-grid {
grid-template-columns: 1fr;
gap: 2rem;
}
.use-cases-grid {
grid-template-columns: 1fr;
}
.features-section,
.use-cases-section {
padding: 60px 20px;
}
.cta-section {
padding: 60px 20px;
}
}
@ -137,11 +262,14 @@
.hero-section h1 {
font-size: 2rem;
}
.hero-section p {
font-size: 0.9rem;
.hero-subtitle {
font-size: 1rem;
}
.hero-ctas .btn-primary, .hero-ctas .btn-outline {
width: 100%;
box-sizing: border-box;
.features-header h2,
.use-cases-section h2,
.cta-section h2 {
font-size: 1.75rem;
}
}

View File

@ -1,9 +1,11 @@
export function showUploadModal() {
document.getElementById('upload-modal').style.display = 'block';
// Clear previous selections and progress
const modal = document.getElementById('upload-modal');
if (modal) {
modal.classList.add('show');
}
document.getElementById('selected-files-preview').innerHTML = '';
document.getElementById('upload-progress-container').innerHTML = '';
document.getElementById('file-input-multiple').value = ''; // Clear selected files
document.getElementById('file-input-multiple').value = '';
document.getElementById('start-upload-btn').disabled = true;
}
@ -13,6 +15,11 @@ document.addEventListener('DOMContentLoaded', () => {
const startUploadBtn = document.getElementById('start-upload-btn');
const uploadProgressContainer = document.getElementById('upload-progress-container');
if (!fileInput || !selectedFilesPreview || !startUploadBtn || !uploadProgressContainer) {
console.error('Upload elements not found');
return;
}
let filesToUpload = [];
fileInput.addEventListener('change', (event) => {
@ -55,62 +62,94 @@ document.addEventListener('DOMContentLoaded', () => {
});
async function uploadFiles(files) {
startUploadBtn.disabled = true; // Disable button during upload
uploadProgressContainer.innerHTML = ''; // Clear previous progress
startUploadBtn.disabled = true;
uploadProgressContainer.innerHTML = '';
const currentPath = new URLSearchParams(window.location.search).get('path') || '';
console.log('Uploading to directory:', currentPath || '(root)');
for (const file of files) {
const formData = new FormData();
formData.append('file', file);
let completedUploads = 0;
let totalFiles = files.length;
let hasErrors = false;
const progressBarContainer = document.createElement('div');
progressBarContainer.className = 'progress-bar-container';
progressBarContainer.innerHTML = `
<div class="file-name">${file.name}</div>
<div class="progress-bar-wrapper">
<div class="progress-bar" id="progress-${file.name.replace(/\./g, '-')}" style="width: 0%;"></div>
</div>
<div class="progress-text" id="progress-text-${file.name.replace(/\./g, '-')}" >0%</div>
`;
uploadProgressContainer.appendChild(progressBarContainer);
const uploadPromises = files.map(file => {
return new Promise((resolve, reject) => {
const formData = new FormData();
formData.append('current_path', currentPath);
formData.append('file', file);
const xhr = new XMLHttpRequest();
xhr.open('POST', `/files/upload?current_path=${encodeURIComponent(currentPath)}`, true);
const progressBarContainer = document.createElement('div');
progressBarContainer.className = 'progress-bar-container';
progressBarContainer.innerHTML = `
<div class="file-name">${file.name}</div>
<div class="progress-bar-wrapper">
<div class="progress-bar" id="progress-${file.name.replace(/\./g, '-')}" style="width: 0%;"></div>
</div>
<div class="progress-text" id="progress-text-${file.name.replace(/\./g, '-')}">0%</div>
`;
uploadProgressContainer.appendChild(progressBarContainer);
xhr.upload.addEventListener('progress', (event) => {
if (event.lengthComputable) {
const percent = (event.loaded / event.total) * 100;
document.getElementById(`progress-${file.name.replace(/\./g, '-')}`).style.width = `${percent}%`;
document.getElementById(`progress-text-${file.name.replace(/\./g, '-')}`).textContent = `${Math.round(percent)}%`;
}
const xhr = new XMLHttpRequest();
xhr.open('POST', `/files/upload`, true);
xhr.upload.addEventListener('progress', (event) => {
if (event.lengthComputable) {
const percent = (event.loaded / event.total) * 100;
const progressBar = document.getElementById(`progress-${file.name.replace(/\./g, '-')}`);
const progressText = document.getElementById(`progress-text-${file.name.replace(/\./g, '-')}`);
if (progressBar) progressBar.style.width = `${percent}%`;
if (progressText) progressText.textContent = `${Math.round(percent)}%`;
}
});
xhr.addEventListener('load', () => {
if (xhr.status === 200) {
console.log(`File ${file.name} uploaded successfully.`);
const progressBar = document.getElementById(`progress-${file.name.replace(/\./g, '-')}`);
const progressText = document.getElementById(`progress-text-${file.name.replace(/\./g, '-')}`);
if (progressBar) progressBar.style.width = '100%';
if (progressText) progressText.textContent = '100% (Done)';
resolve();
} else {
console.error(`Error uploading ${file.name}: ${xhr.statusText}`);
const progressText = document.getElementById(`progress-text-${file.name.replace(/\./g, '-')}`);
const progressBar = document.getElementById(`progress-${file.name.replace(/\./g, '-')}`);
if (progressText) progressText.textContent = `Failed (${xhr.status})`;
if (progressBar) progressBar.style.backgroundColor = 'red';
hasErrors = true;
reject(new Error(`Upload failed with status ${xhr.status}`));
}
});
xhr.addEventListener('error', () => {
console.error(`Network error uploading ${file.name}.`);
const progressText = document.getElementById(`progress-text-${file.name.replace(/\./g, '-')}`);
const progressBar = document.getElementById(`progress-${file.name.replace(/\./g, '-')}`);
if (progressText) progressText.textContent = 'Network Error';
if (progressBar) progressBar.style.backgroundColor = 'red';
hasErrors = true;
reject(new Error('Network error'));
});
xhr.send(formData);
});
});
xhr.addEventListener('load', () => {
if (xhr.status === 200) {
console.log(`File ${file.name} uploaded successfully.`);
// Update progress to 100% on completion
document.getElementById(`progress-${file.name.replace(/\./g, '-')}`).style.width = `100%`;
document.getElementById(`progress-text-${file.name.replace(/\./g, '-')}`).textContent = `100% (Done)`;
try {
await Promise.allSettled(uploadPromises);
setTimeout(() => {
const currentUrl = new URL(window.location.href);
const pathParam = currentUrl.searchParams.get('path');
if (pathParam) {
window.location.href = `/files?path=${encodeURIComponent(pathParam)}`;
} else {
console.error(`Error uploading ${file.name}: ${xhr.statusText}`);
document.getElementById(`progress-text-${file.name.replace(/\./g, '-')}`).textContent = `Failed (${xhr.status})`;
document.getElementById(`progress-${file.name.replace(/\./g, '-')}`).style.backgroundColor = `red`;
window.location.href = '/files';
}
});
xhr.addEventListener('error', () => {
console.error(`Network error uploading ${file.name}.`);
document.getElementById(`progress-text-${file.name.replace(/\./g, '-')}`).textContent = `Network Error`;
document.getElementById(`progress-${file.name.replace(/\./g, '-')}`).style.backgroundColor = `red`;
});
xhr.send(formData);
}, 500);
} catch (error) {
console.error('Error during upload:', error);
startUploadBtn.disabled = false;
}
// After all files are sent, refresh the page to show new files
// A small delay to allow server to process and update file list
setTimeout(() => {
window.location.reload();
}, 1000);
}
});

View File

@ -88,17 +88,23 @@ document.addEventListener('DOMContentLoaded', () => {
// Helper functions for modals
function showNewFolderModal() {
document.getElementById('new-folder-modal').style.display = 'block';
const modal = document.getElementById('new-folder-modal');
if (modal) {
modal.classList.add('show');
}
}
function closeModal(modalId) {
document.getElementById(modalId).style.display = 'none';
const modal = document.getElementById(modalId);
if (modal) {
modal.classList.remove('show');
}
}
window.closeModal = closeModal; // Make it globally accessible
window.closeModal = closeModal;
window.onclick = function(event) {
if (event.target.classList.contains('modal')) {
event.target.style.display = 'none';
event.target.classList.remove('show');
}
}
@ -113,14 +119,13 @@ document.addEventListener('DOMContentLoaded', () => {
const loading = document.getElementById('share-loading');
const shareLinkInput = document.getElementById('share-link-input');
const shareFileName = document.getElementById('share-file-name');
const shareLinksList = document.getElementById('share-links-list'); // New element for multiple links
const shareLinksList = document.getElementById('share-links-list');
// Clear previous content
shareLinkInput.value = '';
if (shareLinksList) shareLinksList.innerHTML = '';
linkContainer.style.display = 'none';
loading.style.display = 'block';
modal.style.display = 'block';
modal.classList.add('show');
if (paths.length === 1) {
shareFileName.textContent = `Sharing: ${names[0]}`;
@ -180,7 +185,6 @@ document.addEventListener('DOMContentLoaded', () => {
const deleteMessage = document.getElementById('delete-message');
const deleteModal = document.getElementById('delete-modal');
// Clear previous hidden inputs
deleteForm.querySelectorAll('input[name="paths[]"]').forEach(input => input.remove());
if (Array.isArray(paths) && paths.length > 1) {
@ -199,7 +203,7 @@ document.addEventListener('DOMContentLoaded', () => {
deleteMessage.textContent = `Are you sure you want to delete "${name}"? This action cannot be undone.`;
deleteForm.action = `/files/delete/${path}`;
}
deleteModal.style.display = 'block';
deleteModal.classList.add('show');
}
// Selection and action buttons

View File

@ -1,3 +1,31 @@
<footer aria-label="Site information and copyright">
<p>&copy; 2025 Retoors. All rights reserved.</p>
<div class="footer-content">
<div class="footer-section">
<h4>Legal</h4>
<ul>
<li><a href="/privacy">Privacy Policy</a></li>
<li><a href="/cookies">Cookie Policy</a></li>
<li><a href="/terms">Terms of Service</a></li>
<li><a href="/impressum">Impressum</a></li>
</ul>
</div>
<div class="footer-section">
<h4>Policies</h4>
<ul>
<li><a href="/aup">Acceptable Use Policy</a></li>
<li><a href="/sla">Service Level Agreement</a></li>
<li><a href="/compliance">Security & Compliance</a></li>
</ul>
</div>
<div class="footer-section">
<h4>User Rights</h4>
<ul>
<li><a href="/user_rights">Data Access & Deletion</a></li>
<li><a href="/support">Contact Support</a></li>
</ul>
</div>
</div>
<div class="footer-bottom">
<p>&copy; 2025 Retoors. All rights reserved.</p>
</div>
</footer>

View File

@ -1,22 +1,26 @@
<header>
<nav aria-label="Main navigation">
<a href="/" class="logo" aria-label="HomeBase Storage home">
<img src="/static/images/retoors-logo.svg" alt="HomeBase Storage" />
<span>HomeBase Storage</span>
</a>
<ul class="nav-links">
<li><a href="/solutions" aria-label="Our Solutions">Solutions</a></li>
<li><a href="/pricing" aria-label="Pricing Plans">Pricing</a></li>
<li><a href="/security" aria-label="Security Information">Security</a></li>
<li><a href="/support" aria-label="Support Page">Support</a></li>
{% if request['user'] %}
<li><a href="/dashboard" aria-label="User Dashboard">Dashboard</a></li>
<li><a href="/files" aria-label="File Browser">File Browser</a></li>
<li><a href="/logout" class="btn-primary-nav" aria-label="Logout">Logout</a></li>
{% else %}
<li><a href="/login" class="btn-outline-nav" aria-label="Sign In to your account">Sign In</a></li>
<li><a href="/register" class="btn-primary-nav" aria-label="Start your free trial">Start Your Free Trial</a></li>
{% endif %}
</ul>
<header class="site-header">
<nav class="site-nav" aria-label="Main navigation">
<div class="nav-container">
<a href="/" class="brand" aria-label="Retoor's Cloud Solutions home">
<span class="brand-text">Retoor's</span>
</a>
<ul class="nav-menu">
<li><a href="/solutions">Solutions</a></li>
<li><a href="/pricing">Pricing</a></li>
<li><a href="/security">Security</a></li>
{% if request['user'] %}
<li><a href="/files">My Files</a></li>
{% endif %}
</ul>
<div class="nav-actions">
{% if request['user'] %}
<a href="/files" class="nav-link">Dashboard</a>
<a href="/logout" class="btn-outline">Logout</a>
{% else %}
<a href="/login" class="nav-link">Sign In</a>
<a href="/register" class="btn-primary">Get Started Free</a>
{% endif %}
</div>
</div>
</nav>
</header>

View File

@ -3,18 +3,17 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Retoors Storage{% endblock %}</title>
<title>{% block title %}Retoors Cloud Solutions{% endblock %}</title>
<link rel="stylesheet" href="/static/css/base.css">
<link rel="stylesheet" href="/static/css/components/footer.css">
<link rel="stylesheet" href="/static/css/components/content_pages.css"> {# Added for content page styling #}
<link rel="stylesheet" href="/static/css/components/content_pages.css">
{% block head %}{% endblock %}
</head>
<body>
<div class="container">
{% block content %}{% endblock %}
</div>
{% include 'components/navigation.html' %}
{% block content %}{% endblock %}
{% include 'components/footer.html' %}
{% include 'components/cookie_banner.html' %}
<script src="/static/js/main.js" type="module"></script>
{% block scripts %}{% endblock %}
</body>

View File

@ -1,12 +1,12 @@
{% extends "layouts/dashboard.html" %}
{% block title %}My Files - Retoor's Cloud Solutions{% endblock %}
{% block title %}{% if current_path %}{{ current_path.split('/')[-1] }} - Retoor's Cloud Solutions{% else %}My Files - Retoor's Cloud Solutions{% endif %}{% endblock %}
{% block dashboard_head %}
<link rel="stylesheet" href="/static/css/components/file_browser.css">
{% endblock %}
{% block page_title %}My Files{% endblock %}
{% block page_title %}{% if current_path %}{{ current_path.split('/')[-1] }}{% else %}My Files{% endif %}{% endblock %}
{% block dashboard_actions %}
<button class="btn-primary" id="new-folder-btn">+ New</button>
@ -55,7 +55,13 @@
<a href="/files?path={{ item.path }}">{{ item.name }}</a>
{% else %}
<img src="/static/images/icon-professionals.svg" alt="File Icon" class="file-icon">
{{ item.name }}
{% if item.is_editable %}
<a href="/editor?path={{ item.path }}">{{ item.name }}</a>
{% elif item.is_viewable %}
<a href="/viewer?path={{item.path}}">{{ item.name }}</a>
{% else %}
{{ item.name }}
{% endif %}
{% endif %}
</td>
<td>{{ user.email }}</td>

View File

@ -1,6 +1,6 @@
{% extends "layouts/base.html" %}
{% block title %}Solutions for Everyone - Retoor's Cloud Solutions{% endblock %}
{% block title %}Secure Cloud Storage for Everyone - Retoor's Cloud Solutions{% endblock %}
{% block head %}
<link rel="stylesheet" href="/static/css/components/index.css">
@ -9,26 +9,114 @@
{% block content %}
<main>
<section class="hero-section">
<h1>Solutions for Everyone</h1>
<p>Solutions for Everyone</p>
<div class="benefits-grid">
<div class="benefit-card family-card">
<img src="/static/images/icon-families.svg" alt="Families Icon" class="icon">
<h3>For Families</h3>
<p>Securely backup and share precious photos and videos. Keep fond memories safe for generations.</p>
<div class="hero-content">
<h1>Your files, safe and accessible everywhere</h1>
<p class="hero-subtitle">Store, sync, and share your files with enterprise-grade security. Access from any device, anytime, anywhere.</p>
<div class="hero-ctas">
<a href="/register" class="btn-primary hero-btn">Get Started Free</a>
<a href="/pricing" class="btn-outline hero-btn">View Pricing</a>
</div>
<div class="benefit-card professional-card">
<img src="/static/images/icon-professionals.svg" alt="Professionals Icon" class="icon">
<h3>For Professionals</h3>
<p>Organize important work documents, collaborate in teams, and access files from anywhere.</p>
<p class="hero-subtext">No credit card required • 10 GB free storage</p>
</div>
</section>
<section class="features-section">
<div class="features-header">
<h2>Everything you need to work smarter</h2>
<p>Powerful features designed to keep your data secure and accessible</p>
</div>
<div class="features-grid">
<div class="feature-card">
<div class="feature-icon">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
</svg>
</div>
<h3>Bank-level Security</h3>
<p>256-bit AES encryption and TLS 1.3 protocol ensure your files stay private and protected</p>
</div>
<div class="benefit-card student-card">
<img src="/static/images/icon-students.svg" alt="Students Icon" class="icon">
<h3>For Students</h3>
<p>Store projects, notes, research papers. Access study materials across your devices.</p>
<div class="feature-card">
<div class="feature-icon">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="2" y="7" width="20" height="14" rx="2" ry="2"/>
<path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/>
</svg>
</div>
<h3>Access Anywhere</h3>
<p>Seamlessly sync across all your devices. Desktop, mobile, or web - your files are always there</p>
</div>
<div class="feature-card">
<div class="feature-icon">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
</svg>
</div>
<h3>Easy Sharing</h3>
<p>Share files and folders with anyone using secure links. Control access with permissions</p>
</div>
<div class="feature-card">
<div class="feature-icon">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>
</svg>
</div>
<h3>High Performance</h3>
<p>Lightning-fast upload and download speeds powered by enterprise infrastructure</p>
</div>
<div class="feature-card">
<div class="feature-icon">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
<polyline points="3.27 6.96 12 12.01 20.73 6.96"/>
<line x1="12" y1="22.08" x2="12" y2="12"/>
</svg>
</div>
<h3>Auto Backup</h3>
<p>Never lose important files. Automatic backups keep multiple versions of your documents</p>
</div>
<div class="feature-card">
<div class="feature-icon">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<polyline points="12 6 12 12 16 14"/>
</svg>
</div>
<h3>24/7 Support</h3>
<p>Expert support team ready to help whenever you need. Email, chat, and phone support available</p>
</div>
</div>
<a href="/solutions" class="btn-primary find-plan-btn">Find Your Perfect Plan</a>
</section>
<section class="use-cases-section">
<h2>Perfect for every need</h2>
<div class="use-cases-grid">
<div class="use-case-card">
<img src="/static/images/icon-families.svg" alt="Families Icon" class="use-case-icon">
<h3>For Families</h3>
<p>Keep precious memories safe. Share photos and videos with family members securely.</p>
</div>
<div class="use-case-card">
<img src="/static/images/icon-professionals.svg" alt="Professionals Icon" class="use-case-icon">
<h3>For Professionals</h3>
<p>Collaborate on projects, share documents, and work from anywhere with confidence.</p>
</div>
<div class="use-case-card">
<img src="/static/images/icon-students.svg" alt="Students Icon" class="use-case-icon">
<h3>For Students</h3>
<p>Store all your coursework, projects, and study materials in one secure place.</p>
</div>
</div>
</section>
<section class="cta-section">
<div class="cta-content">
<h2>Start storing your files securely today</h2>
<p>Join thousands of users who trust Retoor's Cloud Solutions</p>
<a href="/register" class="btn-primary cta-btn">Create Free Account</a>
</div>
</section>
</main>
{% endblock %}

View File

@ -38,6 +38,18 @@ class SiteView(web.View):
return await self.terms()
elif self.request.path == "/privacy":
return await self.privacy()
elif self.request.path == "/cookies":
return await self.cookies()
elif self.request.path == "/impressum":
return await self.impressum()
elif self.request.path == "/user_rights":
return await self.user_rights()
elif self.request.path == "/aup":
return await self.aup()
elif self.request.path == "/sla":
return await self.sla()
elif self.request.path == "/compliance":
return await self.compliance()
elif self.request.path == "/shared":
return await self.shared()
elif self.request.path == "/recent":
@ -92,6 +104,36 @@ class SiteView(web.View):
"pages/privacy.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")}
)
async def cookies(self):
return aiohttp_jinja2.render_template(
"pages/cookies.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")}
)
async def impressum(self):
return aiohttp_jinja2.render_template(
"pages/impressum.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")}
)
async def user_rights(self):
return aiohttp_jinja2.render_template(
"pages/user_rights.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")}
)
async def aup(self):
return aiohttp_jinja2.render_template(
"pages/aup.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")}
)
async def sla(self):
return aiohttp_jinja2.render_template(
"pages/sla.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")}
)
async def compliance(self):
return aiohttp_jinja2.render_template(
"pages/compliance.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")}
)
@login_required
async def shared(self):
return aiohttp_jinja2.render_template(
@ -141,6 +183,15 @@ class FileBrowserView(web.View):
path = self.request.query.get("path", "")
files = await file_service.list_files(user_email, path)
# Determine editable and viewable files based on extension
editable_extensions = {'.txt', '.md', '.py', '.js', '.html', '.css', '.json', '.xml', '.yaml', '.yml', '.ini', '.cfg', '.log', '.sh', '.bat', '.ps1', '.php', '.rb', '.java', '.c', '.cpp', '.h', '.hpp'}
viewable_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg', '.mp4', '.avi', '.mov', '.wmv', '.flv', '.webm', '.mkv', '.mp3', '.wav', '.flac', '.aac', '.ogg', '.m4a'}
for item in files:
if not item['is_dir']:
ext = Path(item['name']).suffix.lower()
item['is_editable'] = ext in editable_extensions
item['is_viewable'] = ext in viewable_extensions
success_message = self.request.query.get("success")
error_message = self.request.query.get("error")

View File

@ -9,35 +9,49 @@ class UploadView(web.View):
async def post(self):
user_email = self.request["user"]["email"]
file_service = self.request.app["file_service"]
# Get current path from query parameter or form data
current_path = self.request.query.get("current_path", "")
current_path = ""
try:
reader = await self.request.multipart()
files_uploaded = []
errors = []
pending_files = []
while True:
field = await reader.next()
if field is None:
break
# Check if the field is a file input
if field.name == "file": # Assuming the input field name is 'file'
if field.name == "current_path":
current_path = (await field.read()).decode('utf-8').strip()
print(f"Upload: current_path received: '{current_path}'")
continue
if field.name == "file":
filename = field.filename
if not filename:
errors.append("Filename is required for one of the files.")
continue
content = await field.read()
# Construct the full file path relative to the user's base directory
full_file_path_for_service = f"{current_path}/{filename}" if current_path else filename
pending_files.append((filename, content))
success = await file_service.upload_file(user_email, full_file_path_for_service, content)
if success:
files_uploaded.append(filename)
else:
errors.append(f"Failed to upload file '{filename}'")
print(f"Upload: Processing {len(pending_files)} files to path: '{current_path}'")
for filename, content in pending_files:
if current_path and not current_path.endswith('/'):
full_file_path_for_service = f"{current_path}/{filename}"
elif current_path:
full_file_path_for_service = f"{current_path}{filename}"
else:
full_file_path_for_service = filename
print(f"Upload: Uploading file to: {full_file_path_for_service}")
success = await file_service.upload_file(user_email, full_file_path_for_service, content)
if success:
files_uploaded.append(filename)
else:
errors.append(f"Failed to upload file '{filename}'")
if errors:
return json_response({"status": "error", "message": "Some files failed to upload", "details": errors}, status=500)