fix: validate uploaded media files
This commit is contained in:
5 files changed
+74
-1
No files matched your search
@@ -46,6 +46,7 @@ function play(filename: string) {
|
|||||||
type="file"
|
type="file"
|
||||||
hidden
|
hidden
|
||||||
multiple
|
multiple
|
||||||
|
accept=".aac,.avi,.flac,.flv,.m4a,.m4v,.mkv,.mov,.mp3,.mp4,.mpeg,.mpg,.oga,.ogg,.opus,.ts,.wav,.webm,.wma"
|
||||||
@change="onFilesPicked"
|
@change="onFilesPicked"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -6,6 +6,12 @@ from zyrarbot.storage.uploads_repository import UploadsRepository
|
|||||||
|
|
||||||
|
|
||||||
class UploadsRepositoryTest(unittest.TestCase):
|
class UploadsRepositoryTest(unittest.TestCase):
|
||||||
|
def test_only_audio_and_video_extensions_are_supported(self):
|
||||||
|
self.assertTrue(UploadsRepository.is_supported_filename("song.MP3"))
|
||||||
|
self.assertTrue(UploadsRepository.is_supported_filename("video.webm"))
|
||||||
|
self.assertFalse(UploadsRepository.is_supported_filename("program.exe"))
|
||||||
|
self.assertFalse(UploadsRepository.is_supported_filename("document.pdf"))
|
||||||
|
|
||||||
def test_legacy_root_files_are_assigned_to_first_guild(self):
|
def test_legacy_root_files_are_assigned_to_first_guild(self):
|
||||||
with tempfile.TemporaryDirectory() as directory:
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
root = Path(directory)
|
root = Path(directory)
|
||||||
|
|||||||
@@ -29,3 +29,24 @@ class MediaProbe:
|
|||||||
except (OSError, ValueError) as error:
|
except (OSError, ValueError) as error:
|
||||||
logger.error("ffprobe nie odczytał długości pliku %s: %s", filepath, error)
|
logger.error("ffprobe nie odczytał długości pliku %s: %s", filepath, error)
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
|
async def is_media(self, filepath: str) -> bool:
|
||||||
|
"""Return whether ffprobe recognizes the file as an audio/video container."""
|
||||||
|
try:
|
||||||
|
process = await asyncio.create_subprocess_exec(
|
||||||
|
"ffprobe",
|
||||||
|
"-v",
|
||||||
|
"error",
|
||||||
|
"-show_entries",
|
||||||
|
"format=format_name",
|
||||||
|
"-of",
|
||||||
|
"default=noprint_wrappers=1:nokey=1",
|
||||||
|
filepath,
|
||||||
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
stderr=asyncio.subprocess.PIPE,
|
||||||
|
)
|
||||||
|
stdout, _ = await process.communicate()
|
||||||
|
return process.returncode == 0 and bool(stdout.decode().strip())
|
||||||
|
except OSError as error:
|
||||||
|
logger.error("Nie udało się uruchomić ffprobe dla %s: %s", filepath, error)
|
||||||
|
return False
|
||||||
@@ -6,6 +6,30 @@ from typing import BinaryIO
|
|||||||
|
|
||||||
from zyrarbot.storage.upload_file_info import UploadFileInfo
|
from zyrarbot.storage.upload_file_info import UploadFileInfo
|
||||||
|
|
||||||
|
SUPPORTED_MEDIA_EXTENSIONS = frozenset(
|
||||||
|
{
|
||||||
|
".aac",
|
||||||
|
".avi",
|
||||||
|
".flac",
|
||||||
|
".flv",
|
||||||
|
".m4a",
|
||||||
|
".m4v",
|
||||||
|
".mkv",
|
||||||
|
".mov",
|
||||||
|
".mp3",
|
||||||
|
".mp4",
|
||||||
|
".mpeg",
|
||||||
|
".mpg",
|
||||||
|
".oga",
|
||||||
|
".ogg",
|
||||||
|
".opus",
|
||||||
|
".ts",
|
||||||
|
".wav",
|
||||||
|
".webm",
|
||||||
|
".wma",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class UploadsRepository:
|
class UploadsRepository:
|
||||||
"""Katalog z plikami wgranymi na serwer; nazwy zawsze sprowadzane do basename."""
|
"""Katalog z plikami wgranymi na serwer; nazwy zawsze sprowadzane do basename."""
|
||||||
@@ -18,6 +42,10 @@ class UploadsRepository:
|
|||||||
def directory(self) -> str:
|
def directory(self) -> str:
|
||||||
return self._directory
|
return self._directory
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_supported_filename(filename: str) -> bool:
|
||||||
|
return os.path.splitext(filename)[1].lower() in SUPPORTED_MEDIA_EXTENSIONS
|
||||||
|
|
||||||
def _guild_directory(self, guild_id: int | None) -> str:
|
def _guild_directory(self, guild_id: int | None) -> str:
|
||||||
if guild_id is None:
|
if guild_id is None:
|
||||||
return self._directory
|
return self._directory
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
|
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
|
||||||
|
|
||||||
|
from zyrarbot.audio.media_probe import MediaProbe
|
||||||
from zyrarbot.storage.uploads_repository import UploadsRepository
|
from zyrarbot.storage.uploads_repository import UploadsRepository
|
||||||
from zyrarbot.ui.api.dependencies import get_active_guild, get_uploads
|
from zyrarbot.ui.api.dependencies import get_active_guild, get_uploads
|
||||||
|
|
||||||
@@ -25,8 +28,22 @@ async def upload(
|
|||||||
guild: discord.Guild = Depends(get_active_guild),
|
guild: discord.Guild = Depends(get_active_guild),
|
||||||
uploads: UploadsRepository = Depends(get_uploads),
|
uploads: UploadsRepository = Depends(get_uploads),
|
||||||
) -> dict[str, str]:
|
) -> dict[str, str]:
|
||||||
|
filename = file.filename or ""
|
||||||
|
if not uploads.is_supported_filename(filename):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=415,
|
||||||
|
detail="Unsupported file type. Upload an audio or video file.",
|
||||||
|
)
|
||||||
|
|
||||||
|
suffix = filename[filename.rfind(".") :]
|
||||||
|
with tempfile.NamedTemporaryFile(suffix=suffix) as temporary:
|
||||||
|
shutil.copyfileobj(file.file, temporary)
|
||||||
|
temporary.flush()
|
||||||
|
if not await MediaProbe().is_media(temporary.name):
|
||||||
|
raise HTTPException(status_code=415, detail="The uploaded file is not valid media")
|
||||||
|
file.file.seek(0)
|
||||||
try:
|
try:
|
||||||
filename = uploads.save(file.filename or "plik", file.file, guild.id)
|
filename = uploads.save(filename, file.file, guild.id)
|
||||||
except OSError as error:
|
except OSError as error:
|
||||||
raise HTTPException(status_code=500, detail=f"Błąd zapisu pliku: {error}") from error
|
raise HTTPException(status_code=500, detail=f"Błąd zapisu pliku: {error}") from error
|
||||||
return {"status": "success", "filename": filename}
|
return {"status": "success", "filename": filename}
|
||||||
|
|||||||
Reference in new issue
Block a user