store asset content type in db, add video support

Signed-off-by: Nicolas Froger <nicolas@kektus.xyz>
This commit is contained in:
Nicolas Froger 2024-07-27 02:47:56 +02:00
commit 25b6dbcd7e
No known key found for this signature in database
13 changed files with 56 additions and 26 deletions

View file

@ -18,5 +18,6 @@ import lombok.With;
public class Asset { public class Asset {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) public Long id; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) public Long id;
public String filename; public String filename;
public String contentType;
@ManyToOne @JoinColumn(name = "post_id") public Post post; @ManyToOne @JoinColumn(name = "post_id") public Post post;
} }

View file

@ -7,5 +7,6 @@ import lombok.With;
@NoArgsConstructor @AllArgsConstructor @With @NoArgsConstructor @AllArgsConstructor @With
public class AssetEntity { public class AssetEntity {
public Long id; public Long id;
public String contentType;
public String presignedUrl; public String presignedUrl;
} }

View file

@ -12,6 +12,7 @@ import io.minio.http.Method;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.transaction.Transactional; import jakarta.transaction.Transactional;
import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.InternalServerErrorException; import jakarta.ws.rs.InternalServerErrorException;
import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.wildfly.common.Assert; import org.wildfly.common.Assert;
@ -32,15 +33,19 @@ public class AssetService {
@ConfigProperty(name = "kektus.assets.bucket") String bucketName; @ConfigProperty(name = "kektus.assets.bucket") String bucketName;
@Transactional @Transactional
public PresignedAsset createAsset(String filename) { public PresignedAsset createAsset(String filename, String contentType) {
if (!contentType.startsWith("image/") && !contentType.startsWith("video/")) {
throw new BadRequestException("forbidden asset content type");
}
String actualFilename = UUID.randomUUID() + "_" + filename; String actualFilename = UUID.randomUUID() + "_" + filename;
PostPolicy policy = new PostPolicy(bucketName, ZonedDateTime.now().plusMinutes(60)); PostPolicy policy = new PostPolicy(bucketName, ZonedDateTime.now().plusMinutes(60));
policy.addEqualsCondition("key", actualFilename); policy.addEqualsCondition("key", actualFilename);
policy.addStartsWithCondition("Content-Type", ""); policy.addEqualsCondition("Content-Type", contentType);
policy.addContentLengthRangeCondition(64 * 1024, 200 * 1024 * 1024); policy.addContentLengthRangeCondition(64 * 1024, 200 * 1024 * 1024);
try { try {
Map<String, String> formData = minioClient.getPresignedPostFormData(policy); Map<String, String> formData = minioClient.getPresignedPostFormData(policy);
Asset asset = new Asset().withFilename(actualFilename); Asset asset = new Asset().withFilename(actualFilename).withContentType(contentType);
assetRepository.persist(asset); assetRepository.persist(asset);
return new PresignedAsset().withId(asset.id).withFilename(actualFilename).withFormData(formData); return new PresignedAsset().withId(asset.id).withFilename(actualFilename).withFormData(formData);
} catch (MinioException | InvalidKeyException | IOException | NoSuchAlgorithmException e) { } catch (MinioException | InvalidKeyException | IOException | NoSuchAlgorithmException e) {

View file

@ -36,6 +36,7 @@ public class PostService {
.withCountry(post.country) .withCountry(post.country)
.withAssets(post.assets.stream().map(asset -> new AssetEntity() .withAssets(post.assets.stream().map(asset -> new AssetEntity()
.withId(asset.id) .withId(asset.id)
.withContentType(asset.contentType)
.withPresignedUrl(assetService.getPresignedUrlForAsset(asset)) .withPresignedUrl(assetService.getPresignedUrlForAsset(asset))
).toList())).toList(); ).toList())).toList();
} }
@ -86,6 +87,7 @@ public class PostService {
.withCountry(post.country) .withCountry(post.country)
.withAssets(post.assets.stream().map(asset -> new AssetEntity() .withAssets(post.assets.stream().map(asset -> new AssetEntity()
.withId(asset.id) .withId(asset.id)
.withContentType(asset.contentType)
.withPresignedUrl(assetService.getPresignedUrlForAsset(asset)) .withPresignedUrl(assetService.getPresignedUrlForAsset(asset))
).toList()); ).toList());
} }

View file

@ -25,29 +25,30 @@ public class AdminAssetApi {
@Inject AuthService authService; @Inject AuthService authService;
@POST @POST
public CreateAssets.Response createAsset(@RestHeader("X-admin-token") final String adminToken, public DTOs.CreateAssetResponse createAsset(@RestHeader("X-admin-token") final String adminToken,
CreateAssets.Request request) { DTOs.CreateAssetRequest request) {
if (!authService.isAdminTokenValid(adminToken)) if (!authService.isAdminTokenValid(adminToken))
throw new NotAuthorizedException("provided admin token is invalid"); throw new NotAuthorizedException("provided admin token is invalid");
Assert.assertNotNull(request); Assert.assertNotNull(request);
Assert.assertNotNull(request.filename); Assert.assertNotNull(request.filename);
final var asset = assetService.createAsset(request.filename); final var asset = assetService.createAsset(request.filename, request.contentType);
return new CreateAssets.Response() return new DTOs.CreateAssetResponse()
.withId(asset.id) .withId(asset.id)
.withFilename(asset.filename) .withFilename(asset.filename)
.withFormData(asset.formData); .withFormData(asset.formData);
} }
public static class CreateAssets { public static class DTOs {
@NoArgsConstructor @AllArgsConstructor @With @NoArgsConstructor @AllArgsConstructor @With
public static class Request { public static class CreateAssetRequest {
public String filename; public String filename;
public String contentType;
} }
@NoArgsConstructor @AllArgsConstructor @With @NoArgsConstructor @AllArgsConstructor @With
public static class Response { public static class CreateAssetResponse {
public Long id; public Long id;
public String filename; public String filename;
public Map<String, String> formData; public Map<String, String> formData;

View file

@ -18,16 +18,16 @@ public class AdminAuthApi {
@Inject AuthService authService; @Inject AuthService authService;
@GET @Path("/check") @GET @Path("/check")
public CheckAuth.Response checkAuth(@RestHeader("X-admin-token") final String adminToken) { public DTOs.CheckAuthResponse checkAuth(@RestHeader("X-admin-token") final String adminToken) {
if (!authService.isAdminTokenValid(adminToken)) if (!authService.isAdminTokenValid(adminToken))
throw new NotAuthorizedException("provided admin token is invalid"); throw new NotAuthorizedException("provided admin token is invalid");
return new CheckAuth.Response().withStatus("ok"); return new DTOs.CheckAuthResponse().withStatus("ok");
} }
public static class CheckAuth { public static class DTOs {
@AllArgsConstructor @NoArgsConstructor @With @AllArgsConstructor @NoArgsConstructor @With
public static class Response { public static class CheckAuthResponse {
public String status; public String status;
} }
} }

View file

@ -30,7 +30,7 @@ public class LocationApi {
@POST @POST
public LocationEntity createPost(@RestHeader("X-admin-token") final String adminToken, public LocationEntity createPost(@RestHeader("X-admin-token") final String adminToken,
CreateLocation.Request request) { DTOs.CreateLocationRequest request) {
if (!authService.isAdminTokenValid(adminToken)) if (!authService.isAdminTokenValid(adminToken))
throw new NotAuthorizedException("provided admin token is invalid"); throw new NotAuthorizedException("provided admin token is invalid");
@ -41,9 +41,9 @@ public class LocationApi {
return locationService.createLocation(request.latitude, request.longitude); return locationService.createLocation(request.latitude, request.longitude);
} }
public static class CreateLocation { public static class DTOs {
@NoArgsConstructor @AllArgsConstructor @With @NoArgsConstructor @AllArgsConstructor @With
public static class Request { public static class CreateLocationRequest {
public Float latitude; public Float latitude;
public Float longitude; public Float longitude;
} }

View file

@ -35,7 +35,7 @@ public class PostApi {
} }
@POST @POST
public PostEntity createPost(@RestHeader("X-admin-token") final String adminToken, CreatePost.Request request) { public PostEntity createPost(@RestHeader("X-admin-token") final String adminToken, DTOs.CreatePostRequest request) {
if (!authService.isAdminTokenValid(adminToken)) if (!authService.isAdminTokenValid(adminToken))
throw new NotAuthorizedException("provided admin token is invalid"); throw new NotAuthorizedException("provided admin token is invalid");
@ -74,7 +74,7 @@ public class PostApi {
@Path("/{id}") @Path("/{id}")
public PostEntity updatePost(@RestHeader("X-admin-token") final String adminToken, public PostEntity updatePost(@RestHeader("X-admin-token") final String adminToken,
@PathParam("id") final Long id, @PathParam("id") final Long id,
final UpdatePost.Request request) { final DTOs.UpdatePostRequest request) {
if (!authService.isAdminTokenValid(adminToken)) if (!authService.isAdminTokenValid(adminToken))
throw new NotAuthorizedException("provided admin token is invalid"); throw new NotAuthorizedException("provided admin token is invalid");
@ -89,9 +89,9 @@ public class PostApi {
); );
} }
public static class CreatePost { public static class DTOs {
@NoArgsConstructor @AllArgsConstructor @With @NoArgsConstructor @AllArgsConstructor @With
public static class Request { public static class CreatePostRequest {
public String description; public String description;
public Float latitude; public Float latitude;
public Float longitude; public Float longitude;
@ -99,11 +99,9 @@ public class PostApi {
public String country; public String country;
public List<Long> assets; public List<Long> assets;
} }
}
public static class UpdatePost {
@NoArgsConstructor @AllArgsConstructor @With @NoArgsConstructor @AllArgsConstructor @With
public static class Request { public static class UpdatePostRequest {
public String description; public String description;
public Float latitude; public Float latitude;
public Float longitude; public Float longitude;

View file

@ -0,0 +1,2 @@
alter table if exists Asset
add column contentType varchar(255);

View file

@ -62,7 +62,18 @@ function postDataToggle() {
<img <img
class="absolute top-0 left-0 w-full h-full object-cover -z-10" class="absolute top-0 left-0 w-full h-full object-cover -z-10"
:data-src="asset.presignedUrl" :data-src="asset.presignedUrl"
v-if="asset.contentType.startsWith('image/')"
/> />
<video
class="absolute top-0 left-0 w-full h-full object-cover -z-10"
loop
muted="true"
playsinline
data-autoplay
v-else
>
<source :src="asset.presignedUrl" :type="asset.contentType" />
</video>
<div <div
class="grid grid-cols-3 grid-rows-5 absolute w-full h-full top-0 left-0 pointer-events-none" class="grid grid-cols-3 grid-rows-5 absolute w-full h-full top-0 left-0 pointer-events-none"
> >

View file

@ -28,7 +28,7 @@ export const useAdminPostsStore = defineStore('adminPosts', () => {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'X-admin-token': authStore.adminToken 'X-admin-token': authStore.adminToken
}, },
body: JSON.stringify({ filename: file.file.name }) body: JSON.stringify({ filename: file.file.name, contentType: file.file.type })
}) })
.catch((e) => { .catch((e) => {
console.log('Contact API asset failed: ' + e) console.log('Contact API asset failed: ' + e)

View file

@ -215,6 +215,9 @@ function updateCityAndCountry() {
<Card> <Card>
<CardContent class="flex aspect-square items-center justify-center p-6"> <CardContent class="flex aspect-square items-center justify-center p-6">
<img :src="file.displayUrl" v-if="file.file.type.startsWith('image/')" /> <img :src="file.displayUrl" v-if="file.file.type.startsWith('image/')" />
<video controls v-else>
<source :src="file.displayUrl" :type="file.file.type" />
</video>
</CardContent> </CardContent>
</Card> </Card>
<div class="grid grid-cols-2 justify-items-center mt-2"> <div class="grid grid-cols-2 justify-items-center mt-2">

View file

@ -225,7 +225,10 @@ function removeAsset(id) {
<div class="flex flex-col"> <div class="flex flex-col">
<Card> <Card>
<CardContent class="flex aspect-square items-center justify-center p-6"> <CardContent class="flex aspect-square items-center justify-center p-6">
<img :src="asset.presignedUrl" /> <img :src="asset.presignedUrl" v-if="asset.contentType.startsWith('image/')" />
<video controls v-else>
<source :src="asset.presignedUrl" :type="asset.contentType" />
</video>
</CardContent> </CardContent>
</Card> </Card>
<div class="grid justify-items-center mt-2"> <div class="grid justify-items-center mt-2">
@ -244,6 +247,9 @@ function removeAsset(id) {
<Card> <Card>
<CardContent class="flex aspect-square items-center justify-center p-6"> <CardContent class="flex aspect-square items-center justify-center p-6">
<img :src="file.displayUrl" v-if="file.file.type.startsWith('image/')" /> <img :src="file.displayUrl" v-if="file.file.type.startsWith('image/')" />
<video controls v-else>
<source :src="file.displayUrl" :type="file.file.type" />
</video>
</CardContent> </CardContent>
</Card> </Card>
<div class="grid grid-cols-2 justify-items-center mt-2"> <div class="grid grid-cols-2 justify-items-center mt-2">