store asset content type in db, add video support
Signed-off-by: Nicolas Froger <nicolas@kektus.xyz>
This commit is contained in:
parent
efde8738a8
commit
25b6dbcd7e
13 changed files with 56 additions and 26 deletions
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
alter table if exists Asset
|
||||||
|
add column contentType varchar(255);
|
||||||
|
|
@ -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"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue