authentik.admin.files.backends.file
1import os 2from collections.abc import Generator, Iterator 3from contextlib import contextmanager 4from datetime import timedelta 5from hashlib import sha256 6from pathlib import Path 7 8import jwt 9from django.conf import settings 10from django.db import connection 11from django.http.request import HttpRequest 12from django.utils.timezone import now 13 14from authentik.admin.files.backends.base import ManageableBackend 15from authentik.admin.files.usage import FileUsage 16from authentik.lib.config import CONFIG 17from authentik.lib.utils.time import timedelta_from_string 18 19 20class FileBackend(ManageableBackend): 21 """Local filesystem backend for file storage. 22 23 Stores files in a local directory structure: 24 - Path: {base_dir}/{usage}/{schema}/{filename} 25 - Supports full file management (upload, delete, list) 26 - Used when storage.backend=file (default) 27 """ 28 29 name = "file" 30 allowed_usages = list(FileUsage) # All usages 31 32 @property 33 def _base_dir(self) -> Path: 34 return Path( 35 CONFIG.get( 36 f"storage.{self.usage.value}.{self.name}.path", 37 CONFIG.get(f"storage.{self.name}.path", "./data"), 38 ) 39 ) 40 41 @property 42 def base_path(self) -> Path: 43 """Path structure: {base_dir}/{usage}/{schema}""" 44 return self._base_dir / self.usage.value / connection.schema_name 45 46 @property 47 def manageable(self) -> bool: 48 # Check _base_dir (the mount point, e.g. /data) rather than base_path 49 # (which includes usage/schema subdirs, e.g. /data/media/public). 50 # The subdirectories are created on first file write via mkdir(parents=True) 51 # in save_file(), so requiring them to exist beforehand would prevent 52 # file creation on fresh installs. 53 return ( 54 self._base_dir.exists() 55 and (self._base_dir.is_mount() or (self._base_dir / self.usage.value).is_mount()) 56 or (settings.DEBUG or settings.TEST) 57 ) 58 59 def supports_file(self, name: str) -> bool: 60 """We support all files""" 61 return True 62 63 def list_files(self) -> Generator[str]: 64 """List all files returning relative paths from base_path.""" 65 for root, _, files in os.walk(self.base_path): 66 for file in files: 67 full_path = Path(root) / file 68 rel_path = full_path.relative_to(self.base_path) 69 yield str(rel_path) 70 71 def file_url( 72 self, 73 name: str, 74 request: HttpRequest | None = None, 75 use_cache: bool = True, 76 ) -> str: 77 """Get URL for accessing the file.""" 78 expires_in = timedelta_from_string( 79 CONFIG.get( 80 f"storage.{self.usage.value}.{self.name}.url_expiry", 81 CONFIG.get(f"storage.{self.name}.url_expiry", "minutes=15"), 82 ) 83 ) 84 85 def _file_url(name: str, request: HttpRequest | None) -> str: 86 prefix = CONFIG.get("web.path", "/")[:-1] 87 path = f"{self.usage.value}/{connection.schema_name}/{name}" 88 token = jwt.encode( 89 payload={ 90 "path": path, 91 "exp": now() + expires_in, 92 "nbf": now() - timedelta(seconds=15), 93 }, 94 key=sha256(f"{settings.SECRET_KEY}:{self.usage}".encode()).hexdigest(), 95 algorithm="HS256", 96 ) 97 url = f"{prefix}/files/{path}?token={token}" 98 if request is None: 99 return url 100 return request.build_absolute_uri(url) 101 102 if use_cache: 103 timeout = int(expires_in.total_seconds()) 104 return self._cache_get_or_set(name, request, _file_url, timeout) 105 else: 106 return _file_url(name, request) 107 108 def save_file(self, name: str, content: bytes) -> None: 109 """Save file to local filesystem.""" 110 path = self.base_path / Path(name) 111 path.parent.mkdir(parents=True, exist_ok=True) 112 with open(path, "w+b") as f: 113 f.write(content) 114 115 @contextmanager 116 def save_file_stream(self, name: str) -> Iterator: 117 """Context manager for streaming file writes to local filesystem.""" 118 path = self.base_path / Path(name) 119 path.parent.mkdir(parents=True, exist_ok=True) 120 with open(path, "wb") as f: 121 yield f 122 123 def delete_file(self, name: str) -> None: 124 """Delete file from local filesystem.""" 125 path = self.base_path / Path(name) 126 path.unlink(missing_ok=True) 127 128 def file_exists(self, name: str) -> bool: 129 """Check if a file exists.""" 130 path = self.base_path / Path(name) 131 return path.exists()
21class FileBackend(ManageableBackend): 22 """Local filesystem backend for file storage. 23 24 Stores files in a local directory structure: 25 - Path: {base_dir}/{usage}/{schema}/{filename} 26 - Supports full file management (upload, delete, list) 27 - Used when storage.backend=file (default) 28 """ 29 30 name = "file" 31 allowed_usages = list(FileUsage) # All usages 32 33 @property 34 def _base_dir(self) -> Path: 35 return Path( 36 CONFIG.get( 37 f"storage.{self.usage.value}.{self.name}.path", 38 CONFIG.get(f"storage.{self.name}.path", "./data"), 39 ) 40 ) 41 42 @property 43 def base_path(self) -> Path: 44 """Path structure: {base_dir}/{usage}/{schema}""" 45 return self._base_dir / self.usage.value / connection.schema_name 46 47 @property 48 def manageable(self) -> bool: 49 # Check _base_dir (the mount point, e.g. /data) rather than base_path 50 # (which includes usage/schema subdirs, e.g. /data/media/public). 51 # The subdirectories are created on first file write via mkdir(parents=True) 52 # in save_file(), so requiring them to exist beforehand would prevent 53 # file creation on fresh installs. 54 return ( 55 self._base_dir.exists() 56 and (self._base_dir.is_mount() or (self._base_dir / self.usage.value).is_mount()) 57 or (settings.DEBUG or settings.TEST) 58 ) 59 60 def supports_file(self, name: str) -> bool: 61 """We support all files""" 62 return True 63 64 def list_files(self) -> Generator[str]: 65 """List all files returning relative paths from base_path.""" 66 for root, _, files in os.walk(self.base_path): 67 for file in files: 68 full_path = Path(root) / file 69 rel_path = full_path.relative_to(self.base_path) 70 yield str(rel_path) 71 72 def file_url( 73 self, 74 name: str, 75 request: HttpRequest | None = None, 76 use_cache: bool = True, 77 ) -> str: 78 """Get URL for accessing the file.""" 79 expires_in = timedelta_from_string( 80 CONFIG.get( 81 f"storage.{self.usage.value}.{self.name}.url_expiry", 82 CONFIG.get(f"storage.{self.name}.url_expiry", "minutes=15"), 83 ) 84 ) 85 86 def _file_url(name: str, request: HttpRequest | None) -> str: 87 prefix = CONFIG.get("web.path", "/")[:-1] 88 path = f"{self.usage.value}/{connection.schema_name}/{name}" 89 token = jwt.encode( 90 payload={ 91 "path": path, 92 "exp": now() + expires_in, 93 "nbf": now() - timedelta(seconds=15), 94 }, 95 key=sha256(f"{settings.SECRET_KEY}:{self.usage}".encode()).hexdigest(), 96 algorithm="HS256", 97 ) 98 url = f"{prefix}/files/{path}?token={token}" 99 if request is None: 100 return url 101 return request.build_absolute_uri(url) 102 103 if use_cache: 104 timeout = int(expires_in.total_seconds()) 105 return self._cache_get_or_set(name, request, _file_url, timeout) 106 else: 107 return _file_url(name, request) 108 109 def save_file(self, name: str, content: bytes) -> None: 110 """Save file to local filesystem.""" 111 path = self.base_path / Path(name) 112 path.parent.mkdir(parents=True, exist_ok=True) 113 with open(path, "w+b") as f: 114 f.write(content) 115 116 @contextmanager 117 def save_file_stream(self, name: str) -> Iterator: 118 """Context manager for streaming file writes to local filesystem.""" 119 path = self.base_path / Path(name) 120 path.parent.mkdir(parents=True, exist_ok=True) 121 with open(path, "wb") as f: 122 yield f 123 124 def delete_file(self, name: str) -> None: 125 """Delete file from local filesystem.""" 126 path = self.base_path / Path(name) 127 path.unlink(missing_ok=True) 128 129 def file_exists(self, name: str) -> bool: 130 """Check if a file exists.""" 131 path = self.base_path / Path(name) 132 return path.exists()
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)
base_path: pathlib.Path
42 @property 43 def base_path(self) -> Path: 44 """Path structure: {base_dir}/{usage}/{schema}""" 45 return self._base_dir / self.usage.value / connection.schema_name
Path structure: {base_dir}/{usage}/{schema}
manageable: bool
47 @property 48 def manageable(self) -> bool: 49 # Check _base_dir (the mount point, e.g. /data) rather than base_path 50 # (which includes usage/schema subdirs, e.g. /data/media/public). 51 # The subdirectories are created on first file write via mkdir(parents=True) 52 # in save_file(), so requiring them to exist beforehand would prevent 53 # file creation on fresh installs. 54 return ( 55 self._base_dir.exists() 56 and (self._base_dir.is_mount() or (self._base_dir / self.usage.value).is_mount()) 57 or (settings.DEBUG or settings.TEST) 58 )
Whether this backend can actually be used for management.
Used only for management check, not for created the backend
def
list_files(self) -> Generator[str]:
64 def list_files(self) -> Generator[str]: 65 """List all files returning relative paths from base_path.""" 66 for root, _, files in os.walk(self.base_path): 67 for file in files: 68 full_path = Path(root) / file 69 rel_path = full_path.relative_to(self.base_path) 70 yield str(rel_path)
List all files returning relative paths from base_path.
def
file_url( self, name: str, request: django.http.request.HttpRequest | None = None, use_cache: bool = True) -> str:
72 def file_url( 73 self, 74 name: str, 75 request: HttpRequest | None = None, 76 use_cache: bool = True, 77 ) -> str: 78 """Get URL for accessing the file.""" 79 expires_in = timedelta_from_string( 80 CONFIG.get( 81 f"storage.{self.usage.value}.{self.name}.url_expiry", 82 CONFIG.get(f"storage.{self.name}.url_expiry", "minutes=15"), 83 ) 84 ) 85 86 def _file_url(name: str, request: HttpRequest | None) -> str: 87 prefix = CONFIG.get("web.path", "/")[:-1] 88 path = f"{self.usage.value}/{connection.schema_name}/{name}" 89 token = jwt.encode( 90 payload={ 91 "path": path, 92 "exp": now() + expires_in, 93 "nbf": now() - timedelta(seconds=15), 94 }, 95 key=sha256(f"{settings.SECRET_KEY}:{self.usage}".encode()).hexdigest(), 96 algorithm="HS256", 97 ) 98 url = f"{prefix}/files/{path}?token={token}" 99 if request is None: 100 return url 101 return request.build_absolute_uri(url) 102 103 if use_cache: 104 timeout = int(expires_in.total_seconds()) 105 return self._cache_get_or_set(name, request, _file_url, timeout) 106 else: 107 return _file_url(name, request)
Get URL for accessing the file.
def
save_file(self, name: str, content: bytes) -> None:
109 def save_file(self, name: str, content: bytes) -> None: 110 """Save file to local filesystem.""" 111 path = self.base_path / Path(name) 112 path.parent.mkdir(parents=True, exist_ok=True) 113 with open(path, "w+b") as f: 114 f.write(content)
Save file to local filesystem.
@contextmanager
def
save_file_stream(self, name: str) -> Iterator:
116 @contextmanager 117 def save_file_stream(self, name: str) -> Iterator: 118 """Context manager for streaming file writes to local filesystem.""" 119 path = self.base_path / Path(name) 120 path.parent.mkdir(parents=True, exist_ok=True) 121 with open(path, "wb") as f: 122 yield f
Context manager for streaming file writes to local filesystem.
def
delete_file(self, name: str) -> None:
124 def delete_file(self, name: str) -> None: 125 """Delete file from local filesystem.""" 126 path = self.base_path / Path(name) 127 path.unlink(missing_ok=True)
Delete file from local filesystem.
def
file_exists(self, name: str) -> bool:
129 def file_exists(self, name: str) -> bool: 130 """Check if a file exists.""" 131 path = self.base_path / Path(name) 132 return path.exists()
Check if a file exists.