mirror of
https://github.com/goauthentik/authentik
synced 2026-04-25 17:15:26 +02:00
132 lines
4.6 KiB
Python
132 lines
4.6 KiB
Python
import os
|
|
from collections.abc import Generator, Iterator
|
|
from contextlib import contextmanager
|
|
from datetime import timedelta
|
|
from hashlib import sha256
|
|
from pathlib import Path
|
|
|
|
import jwt
|
|
from django.conf import settings
|
|
from django.db import connection
|
|
from django.http.request import HttpRequest
|
|
from django.utils.timezone import now
|
|
|
|
from authentik.admin.files.backends.base import ManageableBackend
|
|
from authentik.admin.files.usage import FileUsage
|
|
from authentik.lib.config import CONFIG
|
|
from authentik.lib.utils.time import timedelta_from_string
|
|
|
|
|
|
class FileBackend(ManageableBackend):
|
|
"""Local filesystem backend for file storage.
|
|
|
|
Stores files in a local directory structure:
|
|
- Path: {base_dir}/{usage}/{schema}/{filename}
|
|
- Supports full file management (upload, delete, list)
|
|
- Used when storage.backend=file (default)
|
|
"""
|
|
|
|
name = "file"
|
|
allowed_usages = list(FileUsage) # All usages
|
|
|
|
@property
|
|
def _base_dir(self) -> Path:
|
|
return Path(
|
|
CONFIG.get(
|
|
f"storage.{self.usage.value}.{self.name}.path",
|
|
CONFIG.get(f"storage.{self.name}.path", "./data"),
|
|
)
|
|
)
|
|
|
|
@property
|
|
def base_path(self) -> Path:
|
|
"""Path structure: {base_dir}/{usage}/{schema}"""
|
|
return self._base_dir / self.usage.value / connection.schema_name
|
|
|
|
@property
|
|
def manageable(self) -> bool:
|
|
# Check _base_dir (the mount point, e.g. /data) rather than base_path
|
|
# (which includes usage/schema subdirs, e.g. /data/media/public).
|
|
# The subdirectories are created on first file write via mkdir(parents=True)
|
|
# in save_file(), so requiring them to exist beforehand would prevent
|
|
# file creation on fresh installs.
|
|
return (
|
|
self._base_dir.exists()
|
|
and (self._base_dir.is_mount() or (self._base_dir / self.usage.value).is_mount())
|
|
or (settings.DEBUG or settings.TEST)
|
|
)
|
|
|
|
def supports_file(self, name: str) -> bool:
|
|
"""We support all files"""
|
|
return True
|
|
|
|
def list_files(self) -> Generator[str]:
|
|
"""List all files returning relative paths from base_path."""
|
|
for root, _, files in os.walk(self.base_path):
|
|
for file in files:
|
|
full_path = Path(root) / file
|
|
rel_path = full_path.relative_to(self.base_path)
|
|
yield str(rel_path)
|
|
|
|
def file_url(
|
|
self,
|
|
name: str,
|
|
request: HttpRequest | None = None,
|
|
use_cache: bool = True,
|
|
) -> str:
|
|
"""Get URL for accessing the file."""
|
|
expires_in = timedelta_from_string(
|
|
CONFIG.get(
|
|
f"storage.{self.usage.value}.{self.name}.url_expiry",
|
|
CONFIG.get(f"storage.{self.name}.url_expiry", "minutes=15"),
|
|
)
|
|
)
|
|
|
|
def _file_url(name: str, request: HttpRequest | None) -> str:
|
|
prefix = CONFIG.get("web.path", "/")[:-1]
|
|
path = f"{self.usage.value}/{connection.schema_name}/{name}"
|
|
token = jwt.encode(
|
|
payload={
|
|
"path": path,
|
|
"exp": now() + expires_in,
|
|
"nbf": now() - timedelta(seconds=15),
|
|
},
|
|
key=sha256(f"{settings.SECRET_KEY}:{self.usage}".encode()).hexdigest(),
|
|
algorithm="HS256",
|
|
)
|
|
url = f"{prefix}/files/{path}?token={token}"
|
|
if request is None:
|
|
return url
|
|
return request.build_absolute_uri(url)
|
|
|
|
if use_cache:
|
|
timeout = int(expires_in.total_seconds())
|
|
return self._cache_get_or_set(name, request, _file_url, timeout)
|
|
else:
|
|
return _file_url(name, request)
|
|
|
|
def save_file(self, name: str, content: bytes) -> None:
|
|
"""Save file to local filesystem."""
|
|
path = self.base_path / Path(name)
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
with open(path, "w+b") as f:
|
|
f.write(content)
|
|
|
|
@contextmanager
|
|
def save_file_stream(self, name: str) -> Iterator:
|
|
"""Context manager for streaming file writes to local filesystem."""
|
|
path = self.base_path / Path(name)
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
with open(path, "wb") as f:
|
|
yield f
|
|
|
|
def delete_file(self, name: str) -> None:
|
|
"""Delete file from local filesystem."""
|
|
path = self.base_path / Path(name)
|
|
path.unlink(missing_ok=True)
|
|
|
|
def file_exists(self, name: str) -> bool:
|
|
"""Check if a file exists."""
|
|
path = self.base_path / Path(name)
|
|
return path.exists()
|