mirror of
https://github.com/imgfloat/server.git
synced 2026-02-05 03:39:26 +00:00
Add script asset sub-assets
This commit is contained in:
@@ -134,6 +134,18 @@ public class SchemaMigration implements ApplicationRunner {
|
||||
)
|
||||
"""
|
||||
);
|
||||
jdbcTemplate.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS script_asset_attachments (
|
||||
id TEXT PRIMARY KEY,
|
||||
script_asset_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
media_type TEXT,
|
||||
original_media_type TEXT,
|
||||
asset_type TEXT
|
||||
)
|
||||
"""
|
||||
);
|
||||
backfillAssetTypes(assetColumns);
|
||||
} catch (DataAccessException ex) {
|
||||
logger.warn("Unable to ensure asset type tables", ex);
|
||||
|
||||
@@ -10,6 +10,7 @@ import dev.kruhlmann.imgfloat.model.CanvasSettingsRequest;
|
||||
import dev.kruhlmann.imgfloat.model.CodeAssetRequest;
|
||||
import dev.kruhlmann.imgfloat.model.OauthSessionUser;
|
||||
import dev.kruhlmann.imgfloat.model.PlaybackRequest;
|
||||
import dev.kruhlmann.imgfloat.model.ScriptAssetAttachmentView;
|
||||
import dev.kruhlmann.imgfloat.model.TransformRequest;
|
||||
import dev.kruhlmann.imgfloat.model.TwitchUserProfile;
|
||||
import dev.kruhlmann.imgfloat.model.VisibilityRequest;
|
||||
@@ -387,6 +388,33 @@ public class ChannelApiController {
|
||||
.orElseThrow(() -> createAsset404());
|
||||
}
|
||||
|
||||
@GetMapping("/script-assets/{assetId}/attachments/{attachmentId}/content")
|
||||
public ResponseEntity<byte[]> getScriptAttachmentContent(
|
||||
@PathVariable("broadcaster") String broadcaster,
|
||||
@PathVariable("assetId") String assetId,
|
||||
@PathVariable("attachmentId") String attachmentId
|
||||
) {
|
||||
String logBroadcaster = LogSanitizer.sanitize(broadcaster);
|
||||
String logAssetId = LogSanitizer.sanitize(assetId);
|
||||
String logAttachmentId = LogSanitizer.sanitize(attachmentId);
|
||||
LOG.debug(
|
||||
"Serving script attachment {} for asset {} for broadcaster {}",
|
||||
logAttachmentId,
|
||||
logAssetId,
|
||||
logBroadcaster
|
||||
);
|
||||
return channelDirectoryService
|
||||
.getScriptAttachmentContent(broadcaster, assetId, attachmentId)
|
||||
.map((content) ->
|
||||
ResponseEntity.ok()
|
||||
.header("X-Content-Type-Options", "nosniff")
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, contentDispositionFor(content.mediaType()))
|
||||
.contentType(MediaType.parseMediaType(content.mediaType()))
|
||||
.body(content.bytes())
|
||||
)
|
||||
.orElseThrow(() -> createAsset404());
|
||||
}
|
||||
|
||||
@GetMapping("/assets/{assetId}/preview")
|
||||
public ResponseEntity<byte[]> getAssetPreview(
|
||||
@PathVariable("broadcaster") String broadcaster,
|
||||
@@ -439,6 +467,65 @@ public class ChannelApiController {
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@GetMapping("/assets/{assetId}/attachments")
|
||||
public Collection<ScriptAssetAttachmentView> listScriptAttachments(
|
||||
@PathVariable("broadcaster") String broadcaster,
|
||||
@PathVariable("assetId") String assetId,
|
||||
OAuth2AuthenticationToken oauthToken
|
||||
) {
|
||||
String sessionUsername = OauthSessionUser.from(oauthToken).login();
|
||||
authorizationService.userIsBroadcasterOrChannelAdminForBroadcasterOrThrowHttpError(
|
||||
broadcaster,
|
||||
sessionUsername
|
||||
);
|
||||
return channelDirectoryService.listScriptAttachments(broadcaster, assetId);
|
||||
}
|
||||
|
||||
@PostMapping(value = "/assets/{assetId}/attachments", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
public ResponseEntity<ScriptAssetAttachmentView> createScriptAttachment(
|
||||
@PathVariable("broadcaster") String broadcaster,
|
||||
@PathVariable("assetId") String assetId,
|
||||
@org.springframework.web.bind.annotation.RequestPart("file") MultipartFile file,
|
||||
OAuth2AuthenticationToken oauthToken
|
||||
) {
|
||||
String sessionUsername = OauthSessionUser.from(oauthToken).login();
|
||||
authorizationService.userIsBroadcasterOrChannelAdminForBroadcasterOrThrowHttpError(
|
||||
broadcaster,
|
||||
sessionUsername
|
||||
);
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new ResponseStatusException(BAD_REQUEST, "Attachment file is required");
|
||||
}
|
||||
try {
|
||||
return channelDirectoryService
|
||||
.createScriptAttachment(broadcaster, assetId, file)
|
||||
.map(ResponseEntity::ok)
|
||||
.orElseThrow(() -> new ResponseStatusException(BAD_REQUEST, "Unable to save attachment"));
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to process attachment upload for {} by {}", broadcaster, sessionUsername, e);
|
||||
throw new ResponseStatusException(BAD_REQUEST, "Failed to process attachment", e);
|
||||
}
|
||||
}
|
||||
|
||||
@DeleteMapping("/assets/{assetId}/attachments/{attachmentId}")
|
||||
public ResponseEntity<Void> deleteScriptAttachment(
|
||||
@PathVariable("broadcaster") String broadcaster,
|
||||
@PathVariable("assetId") String assetId,
|
||||
@PathVariable("attachmentId") String attachmentId,
|
||||
OAuth2AuthenticationToken oauthToken
|
||||
) {
|
||||
String sessionUsername = OauthSessionUser.from(oauthToken).login();
|
||||
authorizationService.userIsBroadcasterOrChannelAdminForBroadcasterOrThrowHttpError(
|
||||
broadcaster,
|
||||
sessionUsername
|
||||
);
|
||||
boolean removed = channelDirectoryService.deleteScriptAttachment(broadcaster, assetId, attachmentId);
|
||||
if (!removed) {
|
||||
throw createAsset404();
|
||||
}
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
private ResponseStatusException createAsset404() {
|
||||
return new ResponseStatusException(NOT_FOUND, "Asset not found");
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package dev.kruhlmann.imgfloat.model;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
public record AssetView(
|
||||
String id,
|
||||
@@ -18,6 +19,7 @@ public record AssetView(
|
||||
String mediaType,
|
||||
String originalMediaType,
|
||||
AssetType assetType,
|
||||
List<ScriptAssetAttachmentView> scriptAttachments,
|
||||
Integer zIndex,
|
||||
Boolean audioLoop,
|
||||
Integer audioDelayMillis,
|
||||
@@ -47,6 +49,7 @@ public record AssetView(
|
||||
visual.getMediaType(),
|
||||
visual.getOriginalMediaType(),
|
||||
asset.getAssetType(),
|
||||
null,
|
||||
visual.getZIndex(),
|
||||
null,
|
||||
null,
|
||||
@@ -78,6 +81,7 @@ public record AssetView(
|
||||
audio.getOriginalMediaType(),
|
||||
asset.getAssetType(),
|
||||
null,
|
||||
null,
|
||||
audio.isAudioLoop(),
|
||||
audio.getAudioDelayMillis(),
|
||||
audio.getAudioSpeed(),
|
||||
@@ -107,6 +111,7 @@ public record AssetView(
|
||||
script.getMediaType(),
|
||||
script.getOriginalMediaType(),
|
||||
asset.getAssetType(),
|
||||
script.getAttachments(),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
|
||||
@@ -6,6 +6,8 @@ import jakarta.persistence.Id;
|
||||
import jakarta.persistence.PrePersist;
|
||||
import jakarta.persistence.PreUpdate;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.Transient;
|
||||
import java.util.List;
|
||||
|
||||
@Entity
|
||||
@Table(name = "script_assets")
|
||||
@@ -20,6 +22,9 @@ public class ScriptAsset {
|
||||
private String mediaType;
|
||||
private String originalMediaType;
|
||||
|
||||
@Transient
|
||||
private List<ScriptAssetAttachmentView> attachments = List.of();
|
||||
|
||||
public ScriptAsset() {}
|
||||
|
||||
public ScriptAsset(String assetId, String name) {
|
||||
@@ -66,4 +71,12 @@ public class ScriptAsset {
|
||||
public void setOriginalMediaType(String originalMediaType) {
|
||||
this.originalMediaType = originalMediaType;
|
||||
}
|
||||
|
||||
public List<ScriptAssetAttachmentView> getAttachments() {
|
||||
return attachments == null ? List.of() : attachments;
|
||||
}
|
||||
|
||||
public void setAttachments(List<ScriptAssetAttachmentView> attachments) {
|
||||
this.attachments = attachments;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
package dev.kruhlmann.imgfloat.model;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EnumType;
|
||||
import jakarta.persistence.Enumerated;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.PrePersist;
|
||||
import jakarta.persistence.PreUpdate;
|
||||
import jakarta.persistence.Table;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(name = "script_asset_attachments")
|
||||
public class ScriptAssetAttachment {
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
@Column(name = "script_asset_id", nullable = false)
|
||||
private String scriptAssetId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String name;
|
||||
|
||||
private String mediaType;
|
||||
private String originalMediaType;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "asset_type", nullable = false)
|
||||
private AssetType assetType;
|
||||
|
||||
public ScriptAssetAttachment() {}
|
||||
|
||||
public ScriptAssetAttachment(String scriptAssetId, String name) {
|
||||
this.id = UUID.randomUUID().toString();
|
||||
this.scriptAssetId = scriptAssetId;
|
||||
this.name = name;
|
||||
this.assetType = AssetType.OTHER;
|
||||
}
|
||||
|
||||
@PrePersist
|
||||
@PreUpdate
|
||||
public void prepare() {
|
||||
if (this.id == null) {
|
||||
this.id = UUID.randomUUID().toString();
|
||||
}
|
||||
if (this.name == null || this.name.isBlank()) {
|
||||
this.name = this.id;
|
||||
}
|
||||
if (this.assetType == null) {
|
||||
this.assetType = AssetType.OTHER;
|
||||
}
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getScriptAssetId() {
|
||||
return scriptAssetId;
|
||||
}
|
||||
|
||||
public void setScriptAssetId(String scriptAssetId) {
|
||||
this.scriptAssetId = scriptAssetId;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getMediaType() {
|
||||
return mediaType;
|
||||
}
|
||||
|
||||
public void setMediaType(String mediaType) {
|
||||
this.mediaType = mediaType;
|
||||
}
|
||||
|
||||
public String getOriginalMediaType() {
|
||||
return originalMediaType;
|
||||
}
|
||||
|
||||
public void setOriginalMediaType(String originalMediaType) {
|
||||
this.originalMediaType = originalMediaType;
|
||||
}
|
||||
|
||||
public AssetType getAssetType() {
|
||||
return assetType == null ? AssetType.OTHER : assetType;
|
||||
}
|
||||
|
||||
public void setAssetType(AssetType assetType) {
|
||||
this.assetType = assetType == null ? AssetType.OTHER : assetType;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package dev.kruhlmann.imgfloat.model;
|
||||
|
||||
public record ScriptAssetAttachmentView(
|
||||
String id,
|
||||
String scriptAssetId,
|
||||
String name,
|
||||
String url,
|
||||
String mediaType,
|
||||
String originalMediaType,
|
||||
AssetType assetType
|
||||
) {
|
||||
public static ScriptAssetAttachmentView fromAttachment(String broadcaster, ScriptAssetAttachment attachment) {
|
||||
if (attachment == null) {
|
||||
return null;
|
||||
}
|
||||
return new ScriptAssetAttachmentView(
|
||||
attachment.getId(),
|
||||
attachment.getScriptAssetId(),
|
||||
attachment.getName(),
|
||||
"/api/channels/" + broadcaster + "/script-assets/" + attachment.getScriptAssetId() + "/attachments/" +
|
||||
attachment.getId() +
|
||||
"/content",
|
||||
attachment.getMediaType(),
|
||||
attachment.getOriginalMediaType(),
|
||||
attachment.getAssetType()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package dev.kruhlmann.imgfloat.repository;
|
||||
|
||||
import dev.kruhlmann.imgfloat.model.ScriptAssetAttachment;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface ScriptAssetAttachmentRepository extends JpaRepository<ScriptAssetAttachment, String> {
|
||||
List<ScriptAssetAttachment> findByScriptAssetId(String scriptAssetId);
|
||||
|
||||
List<ScriptAssetAttachment> findByScriptAssetIdIn(Collection<String> scriptAssetIds);
|
||||
|
||||
void deleteByScriptAssetId(String scriptAssetId);
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
package dev.kruhlmann.imgfloat.service;
|
||||
|
||||
import dev.kruhlmann.imgfloat.model.Asset;
|
||||
import dev.kruhlmann.imgfloat.model.ScriptAssetAttachment;
|
||||
import dev.kruhlmann.imgfloat.repository.AssetRepository;
|
||||
import dev.kruhlmann.imgfloat.repository.ScriptAssetAttachmentRepository;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import org.slf4j.Logger;
|
||||
@@ -19,10 +21,16 @@ public class AssetCleanupService {
|
||||
|
||||
private final AssetRepository assetRepository;
|
||||
private final AssetStorageService assetStorageService;
|
||||
private final ScriptAssetAttachmentRepository scriptAssetAttachmentRepository;
|
||||
|
||||
public AssetCleanupService(AssetRepository assetRepository, AssetStorageService assetStorageService) {
|
||||
public AssetCleanupService(
|
||||
AssetRepository assetRepository,
|
||||
AssetStorageService assetStorageService,
|
||||
ScriptAssetAttachmentRepository scriptAssetAttachmentRepository
|
||||
) {
|
||||
this.assetRepository = assetRepository;
|
||||
this.assetStorageService = assetStorageService;
|
||||
this.scriptAssetAttachmentRepository = scriptAssetAttachmentRepository;
|
||||
}
|
||||
|
||||
@Async
|
||||
@@ -31,7 +39,18 @@ public class AssetCleanupService {
|
||||
public void cleanup() {
|
||||
logger.info("Collecting referenced assets");
|
||||
|
||||
Set<String> referencedIds = assetRepository.findAll().stream().map(Asset::getId).collect(Collectors.toSet());
|
||||
Set<String> referencedIds = assetRepository
|
||||
.findAll()
|
||||
.stream()
|
||||
.map(Asset::getId)
|
||||
.collect(Collectors.toSet());
|
||||
referencedIds.addAll(
|
||||
scriptAssetAttachmentRepository
|
||||
.findAll()
|
||||
.stream()
|
||||
.map(ScriptAssetAttachment::getId)
|
||||
.collect(Collectors.toSet())
|
||||
);
|
||||
|
||||
assetStorageService.deleteOrphanedAssets(referencedIds);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ import dev.kruhlmann.imgfloat.model.Channel;
|
||||
import dev.kruhlmann.imgfloat.model.CodeAssetRequest;
|
||||
import dev.kruhlmann.imgfloat.model.PlaybackRequest;
|
||||
import dev.kruhlmann.imgfloat.model.ScriptAsset;
|
||||
import dev.kruhlmann.imgfloat.model.ScriptAssetAttachment;
|
||||
import dev.kruhlmann.imgfloat.model.ScriptAssetAttachmentView;
|
||||
import dev.kruhlmann.imgfloat.model.Settings;
|
||||
import dev.kruhlmann.imgfloat.model.TransformRequest;
|
||||
import dev.kruhlmann.imgfloat.model.VisibilityRequest;
|
||||
@@ -23,6 +25,7 @@ import dev.kruhlmann.imgfloat.repository.AssetRepository;
|
||||
import dev.kruhlmann.imgfloat.repository.AudioAssetRepository;
|
||||
import dev.kruhlmann.imgfloat.repository.ChannelRepository;
|
||||
import dev.kruhlmann.imgfloat.repository.ScriptAssetRepository;
|
||||
import dev.kruhlmann.imgfloat.repository.ScriptAssetAttachmentRepository;
|
||||
import dev.kruhlmann.imgfloat.repository.VisualAssetRepository;
|
||||
import dev.kruhlmann.imgfloat.service.media.AssetContent;
|
||||
import dev.kruhlmann.imgfloat.service.media.MediaDetectionService;
|
||||
@@ -53,6 +56,7 @@ public class ChannelDirectoryService {
|
||||
private final VisualAssetRepository visualAssetRepository;
|
||||
private final AudioAssetRepository audioAssetRepository;
|
||||
private final ScriptAssetRepository scriptAssetRepository;
|
||||
private final ScriptAssetAttachmentRepository scriptAssetAttachmentRepository;
|
||||
private final SimpMessagingTemplate messagingTemplate;
|
||||
private final AssetStorageService assetStorageService;
|
||||
private final MediaDetectionService mediaDetectionService;
|
||||
@@ -67,6 +71,7 @@ public class ChannelDirectoryService {
|
||||
VisualAssetRepository visualAssetRepository,
|
||||
AudioAssetRepository audioAssetRepository,
|
||||
ScriptAssetRepository scriptAssetRepository,
|
||||
ScriptAssetAttachmentRepository scriptAssetAttachmentRepository,
|
||||
SimpMessagingTemplate messagingTemplate,
|
||||
AssetStorageService assetStorageService,
|
||||
MediaDetectionService mediaDetectionService,
|
||||
@@ -79,6 +84,7 @@ public class ChannelDirectoryService {
|
||||
this.visualAssetRepository = visualAssetRepository;
|
||||
this.audioAssetRepository = audioAssetRepository;
|
||||
this.scriptAssetRepository = scriptAssetRepository;
|
||||
this.scriptAssetAttachmentRepository = scriptAssetAttachmentRepository;
|
||||
this.messagingTemplate = messagingTemplate;
|
||||
this.assetStorageService = assetStorageService;
|
||||
this.mediaDetectionService = mediaDetectionService;
|
||||
@@ -220,6 +226,7 @@ public class ChannelDirectoryService {
|
||||
ScriptAsset script = new ScriptAsset(asset.getId(), safeName);
|
||||
script.setMediaType(optimized.mediaType());
|
||||
script.setOriginalMediaType(mediaType);
|
||||
script.setAttachments(List.of());
|
||||
scriptAssetRepository.save(script);
|
||||
view = AssetView.fromScript(channel.getBroadcaster(), asset, script);
|
||||
} else {
|
||||
@@ -261,6 +268,7 @@ public class ChannelDirectoryService {
|
||||
ScriptAsset script = new ScriptAsset(asset.getId(), request.getName().trim());
|
||||
script.setOriginalMediaType(DEFAULT_CODE_MEDIA_TYPE);
|
||||
script.setMediaType(DEFAULT_CODE_MEDIA_TYPE);
|
||||
script.setAttachments(List.of());
|
||||
scriptAssetRepository.save(script);
|
||||
AssetView view = AssetView.fromScript(channel.getBroadcaster(), asset, script);
|
||||
messagingTemplate.convertAndSend(topicFor(broadcaster), AssetEvent.created(broadcaster, view));
|
||||
@@ -286,6 +294,7 @@ public class ChannelDirectoryService {
|
||||
script.setName(request.getName().trim());
|
||||
script.setOriginalMediaType(DEFAULT_CODE_MEDIA_TYPE);
|
||||
script.setMediaType(DEFAULT_CODE_MEDIA_TYPE);
|
||||
script.setAttachments(loadScriptAttachments(normalized, asset.getId(), null));
|
||||
try {
|
||||
assetStorageService.storeAsset(broadcaster, asset.getId(), bytes, DEFAULT_CODE_MEDIA_TYPE);
|
||||
} catch (IOException e) {
|
||||
@@ -341,6 +350,7 @@ public class ChannelDirectoryService {
|
||||
ScriptAsset script = scriptAssetRepository
|
||||
.findById(asset.getId())
|
||||
.orElseThrow(() -> new ResponseStatusException(BAD_REQUEST, "Asset is not a script"));
|
||||
script.setAttachments(loadScriptAttachments(normalized, asset.getId(), null));
|
||||
return AssetView.fromScript(normalized, asset, script);
|
||||
}
|
||||
|
||||
@@ -487,6 +497,7 @@ public class ChannelDirectoryService {
|
||||
ScriptAsset script = scriptAssetRepository
|
||||
.findById(asset.getId())
|
||||
.orElseThrow(() -> new ResponseStatusException(BAD_REQUEST, "Asset is not a script"));
|
||||
script.setAttachments(loadScriptAttachments(normalized, asset.getId(), null));
|
||||
return AssetView.fromScript(normalized, asset, script);
|
||||
}
|
||||
|
||||
@@ -516,7 +527,10 @@ public class ChannelDirectoryService {
|
||||
deleteAssetStorage(asset);
|
||||
switch (asset.getAssetType()) {
|
||||
case AUDIO -> audioAssetRepository.deleteById(asset.getId());
|
||||
case SCRIPT -> scriptAssetRepository.deleteById(asset.getId());
|
||||
case SCRIPT -> {
|
||||
scriptAssetAttachmentRepository.deleteByScriptAssetId(asset.getId());
|
||||
scriptAssetRepository.deleteById(asset.getId());
|
||||
}
|
||||
default -> visualAssetRepository.deleteById(asset.getId());
|
||||
}
|
||||
assetRepository.delete(asset);
|
||||
@@ -533,6 +547,119 @@ public class ChannelDirectoryService {
|
||||
return assetRepository.findById(assetId).flatMap(this::loadAssetContent);
|
||||
}
|
||||
|
||||
public List<ScriptAssetAttachmentView> listScriptAttachments(String broadcaster, String scriptAssetId) {
|
||||
Asset asset = requireScriptAssetForBroadcaster(broadcaster, scriptAssetId);
|
||||
return loadScriptAttachments(normalize(broadcaster), asset.getId(), null);
|
||||
}
|
||||
|
||||
public Optional<ScriptAssetAttachmentView> createScriptAttachment(
|
||||
String broadcaster,
|
||||
String scriptAssetId,
|
||||
MultipartFile file
|
||||
) throws IOException {
|
||||
long fileSize = file.getSize();
|
||||
if (fileSize > uploadLimitBytes) {
|
||||
throw new ResponseStatusException(
|
||||
PAYLOAD_TOO_LARGE,
|
||||
String.format(
|
||||
"Uploaded file is too large (%d bytes). Maximum allowed is %d bytes.",
|
||||
fileSize,
|
||||
uploadLimitBytes
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Asset asset = requireScriptAssetForBroadcaster(broadcaster, scriptAssetId);
|
||||
byte[] bytes = file.getBytes();
|
||||
String mediaType = mediaDetectionService
|
||||
.detectAllowedMediaType(file, bytes)
|
||||
.orElseThrow(() -> new ResponseStatusException(BAD_REQUEST, "Unsupported media type"));
|
||||
|
||||
OptimizedAsset optimized = mediaOptimizationService.optimizeAsset(bytes, mediaType);
|
||||
if (optimized == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
AssetType assetType = AssetType.fromMediaType(optimized.mediaType(), mediaType);
|
||||
if (assetType != AssetType.AUDIO && assetType != AssetType.IMAGE && assetType != AssetType.VIDEO) {
|
||||
throw new ResponseStatusException(BAD_REQUEST, "Only image, video, or audio attachments are supported.");
|
||||
}
|
||||
|
||||
String safeName = Optional.ofNullable(file.getOriginalFilename())
|
||||
.map(this::sanitizeFilename)
|
||||
.filter((s) -> !s.isBlank())
|
||||
.orElse("script_attachment_" + System.currentTimeMillis());
|
||||
|
||||
ScriptAssetAttachment attachment = new ScriptAssetAttachment(asset.getId(), safeName);
|
||||
attachment.setMediaType(optimized.mediaType());
|
||||
attachment.setOriginalMediaType(mediaType);
|
||||
attachment.setAssetType(assetType);
|
||||
|
||||
assetStorageService.storeAsset(asset.getBroadcaster(), attachment.getId(), optimized.bytes(), optimized.mediaType());
|
||||
attachment = scriptAssetAttachmentRepository.save(attachment);
|
||||
ScriptAssetAttachmentView view = ScriptAssetAttachmentView.fromAttachment(asset.getBroadcaster(), attachment);
|
||||
|
||||
ScriptAsset script = scriptAssetRepository
|
||||
.findById(asset.getId())
|
||||
.orElseThrow(() -> new ResponseStatusException(BAD_REQUEST, "Asset is not a script"));
|
||||
script.setAttachments(loadScriptAttachments(asset.getBroadcaster(), asset.getId(), null));
|
||||
AssetView scriptView = AssetView.fromScript(asset.getBroadcaster(), asset, script);
|
||||
messagingTemplate.convertAndSend(topicFor(broadcaster), AssetEvent.updated(broadcaster, scriptView));
|
||||
|
||||
return Optional.of(view);
|
||||
}
|
||||
|
||||
public boolean deleteScriptAttachment(String broadcaster, String scriptAssetId, String attachmentId) {
|
||||
Asset asset = requireScriptAssetForBroadcaster(broadcaster, scriptAssetId);
|
||||
ScriptAssetAttachment attachment = scriptAssetAttachmentRepository
|
||||
.findById(attachmentId)
|
||||
.filter((item) -> item.getScriptAssetId().equals(asset.getId()))
|
||||
.orElse(null);
|
||||
if (attachment == null) {
|
||||
return false;
|
||||
}
|
||||
assetStorageService.deleteAsset(
|
||||
asset.getBroadcaster(),
|
||||
attachment.getId(),
|
||||
attachment.getMediaType(),
|
||||
false
|
||||
);
|
||||
scriptAssetAttachmentRepository.deleteById(attachment.getId());
|
||||
|
||||
ScriptAsset script = scriptAssetRepository
|
||||
.findById(asset.getId())
|
||||
.orElseThrow(() -> new ResponseStatusException(BAD_REQUEST, "Asset is not a script"));
|
||||
script.setAttachments(loadScriptAttachments(asset.getBroadcaster(), asset.getId(), null));
|
||||
AssetView scriptView = AssetView.fromScript(asset.getBroadcaster(), asset, script);
|
||||
messagingTemplate.convertAndSend(topicFor(broadcaster), AssetEvent.updated(broadcaster, scriptView));
|
||||
return true;
|
||||
}
|
||||
|
||||
public Optional<AssetContent> getScriptAttachmentContent(
|
||||
String broadcaster,
|
||||
String scriptAssetId,
|
||||
String attachmentId
|
||||
) {
|
||||
Asset asset = assetRepository
|
||||
.findById(scriptAssetId)
|
||||
.filter((stored) -> normalize(broadcaster).equals(stored.getBroadcaster()))
|
||||
.filter((stored) -> stored.getAssetType() == AssetType.SCRIPT)
|
||||
.orElse(null);
|
||||
if (asset == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return scriptAssetAttachmentRepository
|
||||
.findById(attachmentId)
|
||||
.filter((item) -> item.getScriptAssetId().equals(scriptAssetId))
|
||||
.flatMap((attachment) ->
|
||||
assetStorageService.loadAssetFileSafely(
|
||||
asset.getBroadcaster(),
|
||||
attachment.getId(),
|
||||
attachment.getMediaType()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public Optional<AssetContent> getAssetPreview(String assetId, boolean includeHidden) {
|
||||
return assetRepository.findById(assetId).flatMap((asset) -> loadAssetPreview(asset, includeHidden));
|
||||
}
|
||||
@@ -625,10 +752,24 @@ public class ChannelDirectoryService {
|
||||
.findByIdIn(scriptIds)
|
||||
.stream()
|
||||
.collect(Collectors.toMap(ScriptAsset::getId, (asset) -> asset));
|
||||
Map<String, List<ScriptAssetAttachmentView>> scriptAttachments = scriptIds.isEmpty()
|
||||
? Map.of()
|
||||
: Optional.ofNullable(scriptAssetAttachmentRepository.findByScriptAssetIdIn(scriptIds))
|
||||
.orElse(List.of())
|
||||
.stream()
|
||||
.collect(
|
||||
Collectors.groupingBy(
|
||||
ScriptAssetAttachment::getScriptAssetId,
|
||||
Collectors.mapping(
|
||||
(attachment) -> ScriptAssetAttachmentView.fromAttachment(broadcaster, attachment),
|
||||
Collectors.toList()
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
return assets
|
||||
.stream()
|
||||
.map((asset) -> resolveAssetView(broadcaster, asset, visuals, audios, scripts))
|
||||
.map((asset) -> resolveAssetView(broadcaster, asset, visuals, audios, scripts, scriptAttachments))
|
||||
.filter(Objects::nonNull)
|
||||
.sorted(
|
||||
Comparator.comparing((AssetView view) ->
|
||||
@@ -662,7 +803,7 @@ public class ChannelDirectoryService {
|
||||
}
|
||||
|
||||
private AssetView resolveAssetView(String broadcaster, Asset asset) {
|
||||
return resolveAssetView(broadcaster, asset, null, null, null);
|
||||
return resolveAssetView(broadcaster, asset, null, null, null, null);
|
||||
}
|
||||
|
||||
private AssetView resolveAssetView(
|
||||
@@ -670,7 +811,8 @@ public class ChannelDirectoryService {
|
||||
Asset asset,
|
||||
Map<String, VisualAsset> visuals,
|
||||
Map<String, AudioAsset> audios,
|
||||
Map<String, ScriptAsset> scripts
|
||||
Map<String, ScriptAsset> scripts,
|
||||
Map<String, List<ScriptAssetAttachmentView>> scriptAttachments
|
||||
) {
|
||||
if (asset.getAssetType() == AssetType.AUDIO) {
|
||||
AudioAsset audio = audios != null
|
||||
@@ -682,6 +824,9 @@ public class ChannelDirectoryService {
|
||||
ScriptAsset script = scripts != null
|
||||
? scripts.get(asset.getId())
|
||||
: scriptAssetRepository.findById(asset.getId()).orElse(null);
|
||||
if (script != null) {
|
||||
script.setAttachments(loadScriptAttachments(broadcaster, asset.getId(), scriptAttachments));
|
||||
}
|
||||
return script == null ? null : AssetView.fromScript(broadcaster, asset, script);
|
||||
}
|
||||
VisualAsset visual = visuals != null
|
||||
@@ -728,6 +873,33 @@ public class ChannelDirectoryService {
|
||||
}
|
||||
}
|
||||
|
||||
private List<ScriptAssetAttachmentView> loadScriptAttachments(
|
||||
String broadcaster,
|
||||
String scriptAssetId,
|
||||
Map<String, List<ScriptAssetAttachmentView>> scriptAttachments
|
||||
) {
|
||||
if (scriptAttachments != null) {
|
||||
return scriptAttachments.getOrDefault(scriptAssetId, List.of());
|
||||
}
|
||||
List<ScriptAssetAttachment> attachments = Optional.ofNullable(
|
||||
scriptAssetAttachmentRepository.findByScriptAssetId(scriptAssetId)
|
||||
)
|
||||
.orElse(List.of());
|
||||
return attachments
|
||||
.stream()
|
||||
.map((attachment) -> ScriptAssetAttachmentView.fromAttachment(broadcaster, attachment))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private Asset requireScriptAssetForBroadcaster(String broadcaster, String scriptAssetId) {
|
||||
String normalized = normalize(broadcaster);
|
||||
return assetRepository
|
||||
.findById(scriptAssetId)
|
||||
.filter((asset) -> normalized.equals(asset.getBroadcaster()))
|
||||
.filter((asset) -> asset.getAssetType() == AssetType.SCRIPT)
|
||||
.orElseThrow(() -> new ResponseStatusException(BAD_REQUEST, "Asset is not a script"));
|
||||
}
|
||||
|
||||
private Optional<AssetContent> loadAssetPreview(Asset asset, boolean includeHidden) {
|
||||
if (
|
||||
asset.getAssetType() != AssetType.VIDEO &&
|
||||
@@ -757,9 +929,19 @@ public class ChannelDirectoryService {
|
||||
);
|
||||
case SCRIPT -> scriptAssetRepository
|
||||
.findById(asset.getId())
|
||||
.ifPresent((script) ->
|
||||
assetStorageService.deleteAsset(asset.getBroadcaster(), asset.getId(), script.getMediaType(), false)
|
||||
);
|
||||
.ifPresent((script) -> {
|
||||
assetStorageService.deleteAsset(asset.getBroadcaster(), asset.getId(), script.getMediaType(), false);
|
||||
scriptAssetAttachmentRepository
|
||||
.findByScriptAssetId(asset.getId())
|
||||
.forEach((attachment) ->
|
||||
assetStorageService.deleteAsset(
|
||||
asset.getBroadcaster(),
|
||||
attachment.getId(),
|
||||
attachment.getMediaType(),
|
||||
false
|
||||
)
|
||||
);
|
||||
});
|
||||
default -> visualAssetRepository
|
||||
.findById(asset.getId())
|
||||
.ifPresent((visual) ->
|
||||
|
||||
Reference in New Issue
Block a user