files.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. # Copyright (C) 2025 AIDC-AI
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. # http://www.apache.org/licenses/LICENSE-2.0
  7. # Unless required by applicable law or agreed to in writing, software
  8. # distributed under the License is distributed on an "AS IS" BASIS,
  9. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. # See the License for the specific language governing permissions and
  11. # limitations under the License.
  12. """
  13. File service endpoints
  14. Provides access to generated files (videos, images, audio) and resource files.
  15. """
  16. from pathlib import Path
  17. from fastapi import APIRouter, HTTPException
  18. from fastapi.responses import FileResponse
  19. from loguru import logger
  20. router = APIRouter(prefix="/files", tags=["Files"])
  21. @router.get("/{file_path:path}")
  22. async def get_file(file_path: str):
  23. """
  24. Get file by path
  25. Serves files from allowed directories:
  26. - output/ - Generated files (videos, images, audio)
  27. - workflows/ - ComfyUI workflow files
  28. - templates/ - HTML templates
  29. - bgm/ - Background music
  30. - data/bgm/ - Custom background music
  31. - data/templates/ - Custom templates
  32. - resources/ - Other resources (images, fonts, etc.)
  33. - **file_path**: File path relative to allowed directories
  34. Examples:
  35. - "abc123.mp4" → output/abc123.mp4
  36. - "workflows/runninghub/image_flux.json" → workflows/runninghub/image_flux.json
  37. - "templates/1080x1920/default.html" → templates/1080x1920/default.html
  38. - "bgm/default.mp3" → bgm/default.mp3
  39. - "resources/example.png" → resources/example.png
  40. Returns file for download or preview.
  41. """
  42. try:
  43. # Define allowed directories (in priority order)
  44. allowed_prefixes = [
  45. "output/",
  46. "workflows/",
  47. "templates/",
  48. "bgm/",
  49. "data/bgm/",
  50. "data/templates/",
  51. "resources/",
  52. ]
  53. # Check if path starts with allowed prefix, otherwise try output/
  54. full_path = None
  55. for prefix in allowed_prefixes:
  56. if file_path.startswith(prefix):
  57. full_path = file_path
  58. break
  59. # If no prefix matched, assume it's in output/ (backward compatibility)
  60. if full_path is None:
  61. full_path = f"output/{file_path}"
  62. abs_path = Path.cwd() / full_path
  63. if not abs_path.exists():
  64. raise HTTPException(status_code=404, detail=f"File not found: {file_path}")
  65. if not abs_path.is_file():
  66. raise HTTPException(status_code=400, detail=f"Path is not a file: {file_path}")
  67. # Security: only allow access to specified directories
  68. try:
  69. rel_path = abs_path.relative_to(Path.cwd())
  70. rel_path_str = str(rel_path)
  71. # Check if path starts with any allowed prefix
  72. is_allowed = any(rel_path_str.startswith(prefix.rstrip('/')) for prefix in allowed_prefixes)
  73. if not is_allowed:
  74. raise HTTPException(
  75. status_code=403,
  76. detail=f"Access denied: only {', '.join(p.rstrip('/') for p in allowed_prefixes)} directories are accessible"
  77. )
  78. except ValueError:
  79. raise HTTPException(status_code=403, detail="Access denied")
  80. # Determine media type
  81. suffix = abs_path.suffix.lower()
  82. media_types = {
  83. '.mp4': 'video/mp4',
  84. '.mp3': 'audio/mpeg',
  85. '.wav': 'audio/wav',
  86. '.png': 'image/png',
  87. '.jpg': 'image/jpeg',
  88. '.jpeg': 'image/jpeg',
  89. '.gif': 'image/gif',
  90. '.html': 'text/html',
  91. '.json': 'application/json',
  92. }
  93. media_type = media_types.get(suffix, 'application/octet-stream')
  94. # Use inline disposition for browser preview
  95. return FileResponse(
  96. path=str(abs_path),
  97. media_type=media_type,
  98. headers={
  99. "Content-Disposition": f'inline; filename="{abs_path.name}"'
  100. }
  101. )
  102. except HTTPException:
  103. raise
  104. except Exception as e:
  105. logger.error(f"File access error: {e}")
  106. raise HTTPException(status_code=500, detail=str(e))