Download a backup file. Requires either: - Valid admin authentication, OR - Valid download_token query parameter
(request, filename)
| 154 | @api_view(["GET"]) |
| 155 | @permission_classes([AllowAny]) |
| 156 | def download_backup(request, filename): |
| 157 | """Download a backup file. |
| 158 | |
| 159 | Requires either: |
| 160 | - Valid admin authentication, OR |
| 161 | - Valid download_token query parameter |
| 162 | """ |
| 163 | # Check for token-based auth (avoids CORS preflight issues) |
| 164 | token = request.query_params.get("token") |
| 165 | if token: |
| 166 | if not _verify_task_token(filename, token): |
| 167 | return Response( |
| 168 | {"detail": "Invalid download token"}, |
| 169 | status=status.HTTP_403_FORBIDDEN, |
| 170 | ) |
| 171 | else: |
| 172 | # Fall back to admin auth check |
| 173 | if not request.user.is_authenticated or getattr(request.user, 'user_level', 0) < 10: |
| 174 | return Response( |
| 175 | {"detail": "Authentication required"}, |
| 176 | status=status.HTTP_401_UNAUTHORIZED, |
| 177 | ) |
| 178 | |
| 179 | try: |
| 180 | # Security: prevent path traversal by checking for suspicious characters |
| 181 | if ".." in filename or "/" in filename or "\\" in filename: |
| 182 | raise Http404("Invalid filename") |
| 183 | |
| 184 | backup_dir = services.get_backup_dir() |
| 185 | backup_file = (backup_dir / filename).resolve() |
| 186 | |
| 187 | # Security: ensure the resolved path is still within backup_dir |
| 188 | if not str(backup_file).startswith(str(backup_dir.resolve())): |
| 189 | raise Http404("Invalid filename") |
| 190 | |
| 191 | if not backup_file.exists() or not backup_file.is_file(): |
| 192 | raise Http404("Backup file not found") |
| 193 | |
| 194 | file_size = backup_file.stat().st_size |
| 195 | |
| 196 | # Use X-Accel-Redirect for nginx (AIO container) - nginx serves file directly |
| 197 | # Fall back to streaming for non-nginx deployments |
| 198 | use_nginx_accel = os.environ.get("USE_NGINX_ACCEL", "").lower() == "true" |
| 199 | logger.info(f"[DOWNLOAD] File: {filename}, Size: {file_size}, USE_NGINX_ACCEL: {use_nginx_accel}") |
| 200 | |
| 201 | if use_nginx_accel: |
| 202 | # X-Accel-Redirect: Django returns immediately, nginx serves file |
| 203 | logger.info(f"[DOWNLOAD] Using X-Accel-Redirect: /protected-backups/{filename}") |
| 204 | response = HttpResponse() |
| 205 | response["X-Accel-Redirect"] = f"/protected-backups/{filename}" |
| 206 | response["Content-Type"] = "application/zip" |
| 207 | response["Content-Length"] = file_size |
| 208 | response["Content-Disposition"] = f'attachment; filename="{filename}"' |
| 209 | return response |
| 210 | else: |
| 211 | # Streaming fallback for non-nginx deployments |
| 212 | logger.info(f"[DOWNLOAD] Using streaming fallback (no nginx)") |
| 213 | def file_iterator(file_path, chunk_size=2 * 1024 * 1024): |
nothing calls this directly
no test coverage detected