Materials: images with captions and video support

- markdown: new directives `:::figure assets/x.png | Подпись` and
  `:::video assets/x.mp4 | Подпись` rendered as figure/figcaption and a
  video player (works with HTML escaping on); sanitizer allows video/source/
  track/figure with safe attributes only
- assets: video extensions (mp4/webm/ogg) allowed, separate limits — images
  up to 10 MB, video up to 100 MB; correct content types
- upload limits raised: web/academic multipart 120 MB per file / 200 MB per
  request, nginx client_max_body_size 200m (covers media inside ZIP imports)
- docs: new docs/06-authoring.md (markdown, images, video, assets, limits,
  import semantics); import message now says "медиафайлов"
This commit is contained in:
2026-09-17 15:44:16 +03:00
parent 1238265c8e
commit 396221047c
8 changed files with 200 additions and 17 deletions

View File

@@ -18,13 +18,19 @@ class AssetStorage(@Value("\${lms.content.assets-dir:/data/content-assets}") roo
data class StoredAsset(val publicName: String, val storedPath: Path, val mime: String)
fun store(originalName: String, bytes: ByteArray): StoredAsset {
if (bytes.size > MAX_ASSET_BYTES) {
throw BadRequestException("Файл $originalName слишком большой (> ${MAX_ASSET_BYTES / 1024 / 1024} МБ)")
}
val ext = originalName.substringAfterLast('.', "").lowercase()
val allowed = setOf("png", "jpg", "jpeg", "gif", "svg", "webp")
if (ext !in allowed) {
throw BadRequestException("Недопустимый тип файла: $originalName (разрешены ${allowed.joinToString(", ")})")
val isVideo = ext in VIDEO_EXTENSIONS
val isImage = ext in IMAGE_EXTENSIONS
if (!isVideo && !isImage) {
throw BadRequestException(
"Недопустимый тип файла: $originalName " +
"(изображения: ${IMAGE_EXTENSIONS.joinToString(", ")}; " +
"видео: ${VIDEO_EXTENSIONS.joinToString(", ")})",
)
}
val limit = if (isVideo) MAX_VIDEO_BYTES else MAX_IMAGE_BYTES
if (bytes.size > limit) {
throw BadRequestException("Файл $originalName слишком большой (> ${limit / 1024 / 1024} МБ)")
}
val publicName = "${UUID.randomUUID()}.$ext"
val target = rootDir.resolve(publicName).normalize()
@@ -50,10 +56,16 @@ class AssetStorage(@Value("\${lms.content.assets-dir:/data/content-assets}") roo
"gif" -> "image/gif"
"svg" -> "image/svg+xml"
"webp" -> "image/webp"
"mp4" -> "video/mp4"
"webm" -> "video/webm"
"ogg", "ogv" -> "video/ogg"
else -> "application/octet-stream"
}
private companion object {
const val MAX_ASSET_BYTES = 10 * 1024 * 1024
val IMAGE_EXTENSIONS = setOf("png", "jpg", "jpeg", "gif", "svg", "webp")
val VIDEO_EXTENSIONS = setOf("mp4", "webm", "ogg", "ogv")
const val MAX_IMAGE_BYTES = 10 * 1024 * 1024 // 10 МБ на картинку
const val MAX_VIDEO_BYTES = 100 * 1024 * 1024 // 100 МБ на видео
}
}

View File

@@ -4,6 +4,10 @@ server:
spring:
application:
name: academic-service
servlet:
multipart:
max-file-size: 120MB
max-request-size: 200MB
datasource:
url: ${LMS_DB_URL:jdbc:postgresql://localhost:5432/lms?currentSchema=academic}
username: ${LMS_DB_USER:lms}