diff --git a/Dockerfile b/Dockerfile
index 8a2024d..a4d72ae 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -83,7 +83,7 @@ RUN pnpm static:build
#FROM bellsoft/liberica-openjdk-alpine:latest
## add wget to manage the health check...
#RUN apk add --no-cache wget
-FROM common
+FROM common AS prod
ENV LANG C.UTF-8
@@ -98,4 +98,6 @@ EXPOSE 80
HEALTHCHECK --start-period=10s --start-interval=2s --interval=30s --timeout=5s --retries=10 \
CMD wget --no-verbose --tries=1 --spider http://localhost:80/api/health_check || exit 1
-CMD ["java", "-Xms64M", "-Xmx1G", "-cp", "/application/application.jar", "org.atriasoft.karideo.WebLauncher"]
+CMD ["java", "-Xms128M", "-Xmx1G", "-cp", "/application/application.jar", "org.atriasoft.karideo.WebLauncher"]
+
+#RUN cat /etc/passwd
\ No newline at end of file
diff --git a/back/CheckStyle.xml b/back/CheckStyle.xml
index d68aedd..ac24ac1 100755
--- a/back/CheckStyle.xml
+++ b/back/CheckStyle.xml
@@ -53,6 +53,9 @@ Checkstyle configuration that checks the sun coding conventions.
+
+
+
diff --git a/back/pom.xml.versionsBackup b/back/pom.xml.versionsBackup
new file mode 100644
index 0000000..b59ba80
--- /dev/null
+++ b/back/pom.xml.versionsBackup
@@ -0,0 +1,245 @@
+
+
+ 4.0.0
+ org.kar
+ karideo
+ 0.3.0
+
+ 3.1
+ 21
+ 21
+ 3.1.1
+
+
+
+ gitea
+ https://gitea.atria-soft.org/api/packages/kangaroo-and-rabbit/maven
+
+
+
+
+ kangaroo-and-rabbit
+ archidata
+ 0.11.0
+
+
+ org.slf4j
+ slf4j-simple
+ 2.0.9
+
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-jsr310
+ 2.16.1
+
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ 5.10.1
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ 5.10.1
+ test
+
+
+
+ src
+ test/src
+ ${project.basedir}/out/maven/
+
+
+ src/resources
+
+
+
+
+ ${basedir}/test/resources
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ ${maven.compiler.version}
+
+ ${maven.compiler.source}
+ ${maven.compiler.target}
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ 1.4.0
+
+ org.kar.karideo.WebLauncher
+
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 3.2.1
+
+
+ attach-sources
+
+ jar
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.0.0-M5
+
+
+ maven-assembly-plugin
+
+
+
+ fully.qualified.MainClass
+
+
+
+ jar-with-dependencies
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 3.2.0
+
+ private
+ true
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ 3.1.0
+
+
+ exec-application
+ package
+
+ java
+
+
+
+
+ org.kar.karideo.WebLauncher
+
+
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 3.2.0
+
+ public
+
+
+
+
+
diff --git a/back/src/org/atriasoft/karideo/CacheFilter.java__ b/back/src/org/atriasoft/karideo/CacheFilter.java__
new file mode 100644
index 0000000..4269fc6
--- /dev/null
+++ b/back/src/org/atriasoft/karideo/CacheFilter.java__
@@ -0,0 +1,20 @@
+package org.kar.karideo;
+
+public class CacheFilter {
+ @Override
+ public List create(AbstractMethod am) {
+ if (am.isAnnotationPresent(CacheMaxAge.class)) {
+ CacheMaxAge maxAge = am.getAnnotation(CacheMaxAge.class);
+ return newCacheFilter("max-age: " + maxAge.unit().toSeconds(maxAge.time()));
+ } else if (am.isAnnotationPresent(NoCache.class)) {
+ return newCacheFilter("no-cache");
+ } else {
+ return Collections.emptyList();
+ }
+ }
+
+ private List newCacheFilter(String content) {
+ return Collections
+ . singletonList(new CacheResponseFilter(content));
+ }
+}
diff --git a/back/src/org/atriasoft/karideo/WebLauncher.java b/back/src/org/atriasoft/karideo/WebLauncher.java
index 332ba8e..68ddbce 100755
--- a/back/src/org/atriasoft/karideo/WebLauncher.java
+++ b/back/src/org/atriasoft/karideo/WebLauncher.java
@@ -1,10 +1,10 @@
package org.atriasoft.karideo;
import java.net.URI;
+import java.util.TimeZone;
import java.util.logging.LogManager;
import org.atriasoft.archidata.UpdateJwtPublicKey;
-import org.atriasoft.archidata.api.DataResource;
import org.atriasoft.archidata.catcher.GenericCatcher;
import org.atriasoft.archidata.db.DbConfig;
import org.atriasoft.archidata.filter.CORSFilter;
@@ -12,6 +12,7 @@ import org.atriasoft.archidata.filter.OptionFilter;
import org.atriasoft.archidata.migration.MigrationEngine;
import org.atriasoft.archidata.tools.ConfigBaseVariable;
import org.atriasoft.archidata.tools.ContextGenericTools;
+import org.atriasoft.karideo.api.DataResource;
import org.atriasoft.karideo.api.Front;
import org.atriasoft.karideo.api.HealthCheck;
import org.atriasoft.karideo.api.MediaResource;
@@ -47,6 +48,7 @@ public class WebLauncher {
protected HttpServer server = null;
public WebLauncher() {
+ TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
ConfigBaseVariable.bdDatabase = "karideo";
}
@@ -126,6 +128,10 @@ public class WebLauncher {
this.server = GrizzlyHttpServerFactory.createHttpServer(getBaseURI(), rc);
final HttpServer serverLink = this.server;
+ // for (final NetworkListener listener : serverLink.getListeners()) {
+ // listener.getKeepAlive().setIdleTimeoutInSeconds(30); // Set idle timeout
+ // listener.getKeepAlive().setMaxRequestsCount(80); // Set request timeout
+ // }
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
@Override
public void run() {
diff --git a/back/src/org/atriasoft/karideo/api/DataResource.java b/back/src/org/atriasoft/karideo/api/DataResource.java
new file mode 100644
index 0000000..a4f3394
--- /dev/null
+++ b/back/src/org/atriasoft/karideo/api/DataResource.java
@@ -0,0 +1,531 @@
+package org.atriasoft.karideo.api;
+
+import java.awt.Graphics2D;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.RandomAccessFile;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Date;
+import java.util.UUID;
+
+import javax.imageio.ImageIO;
+
+import org.atriasoft.archidata.annotation.apiGenerator.ApiInputOptional;
+import org.atriasoft.archidata.annotation.security.PermitTokenInURI;
+import org.atriasoft.archidata.api.MediaStreamer;
+import org.atriasoft.archidata.dataAccess.DataAccess;
+import org.atriasoft.archidata.dataAccess.QueryCondition;
+import org.atriasoft.archidata.dataAccess.options.Condition;
+import org.atriasoft.archidata.exception.FailException;
+import org.atriasoft.archidata.filter.GenericContext;
+import org.atriasoft.archidata.model.Data;
+import org.atriasoft.archidata.tools.ConfigBaseVariable;
+import org.bson.types.ObjectId;
+import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
+import org.glassfish.jersey.media.multipart.FormDataParam;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.swagger.v3.oas.annotations.Operation;
+import jakarta.annotation.security.RolesAllowed;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.HeaderParam;
+import jakarta.ws.rs.InternalServerErrorException;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.QueryParam;
+import jakarta.ws.rs.core.CacheControl;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.HttpHeaders;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.SecurityContext;
+import jakarta.ws.rs.core.StreamingOutput;
+
+// https://stackoverflow.com/questions/35367113/jersey-webservice-scalable-approach-to-download-file-and-reply-to-client
+// https://gist.github.com/aitoroses/4f7a2b197b732a6a691d
+
+@Path("/data")
+@Produces(MediaType.APPLICATION_JSON)
+public class DataResource {
+ private static final Logger LOGGER = LoggerFactory.getLogger(DataResource.class);
+ private final static int CHUNK_SIZE = 1024 * 1024; // 1MB chunks
+ private final static int CHUNK_SIZE_IN = 50 * 1024 * 1024; // 1MB chunks
+ /** Upload some datas */
+ private static long tmpFolderId = 1;
+
+ private static void createFolder(final String path) throws IOException {
+ if (!Files.exists(java.nio.file.Path.of(path))) {
+ // Log.print("Create folder: " + path);
+ Files.createDirectories(java.nio.file.Path.of(path));
+ }
+ }
+
+ public static long getTmpDataId() {
+ return tmpFolderId++;
+ }
+
+ public static String getTmpFileInData(final long tmpFolderId) {
+ final String filePath = ConfigBaseVariable.getTmpDataFolder() + File.separator + tmpFolderId;
+ try {
+ createFolder(ConfigBaseVariable.getTmpDataFolder() + File.separator);
+ } catch (final IOException e) {
+ e.printStackTrace();
+ }
+ return filePath;
+ }
+
+ public static String getFileDataOld(final UUID uuid) {
+ final String stringUUID = uuid.toString();
+ final String part1 = stringUUID.substring(0, 2);
+ final String part2 = stringUUID.substring(2, 4);
+ final String part3 = stringUUID.substring(4);
+ final String finalPath = part1 + File.separator + part2;
+ String filePath = ConfigBaseVariable.getMediaDataFolder() + "_uuid" + File.separator + finalPath
+ + File.separator;
+ try {
+ createFolder(filePath);
+ } catch (final IOException e) {
+ e.printStackTrace();
+ }
+ filePath += part3;
+ return filePath;
+ }
+
+ public static String getFileData(final ObjectId oid) {
+ final String stringOid = oid.toHexString();
+ String dir1 = stringOid.substring(0, 2);
+ String dir2 = stringOid.substring(2, 4);
+ String dir3 = stringOid.substring(4, 6);
+ try {
+ final MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ final byte[] hashBytes = digest.digest(oid.toByteArray());
+ dir1 = String.format("%02x", hashBytes[0]);
+ dir2 = String.format("%02x", hashBytes[1]);
+ dir3 = String.format("%02x", hashBytes[2]);
+ } catch (final NoSuchAlgorithmException ex) {
+ LOGGER.error("Fail to generate the hash of the objectId ==> ise direct value ... {}", ex.getMessage());
+ }
+ final String finalPath = dir1 + File.separator + dir2 + File.separator + dir3;
+ String filePath = ConfigBaseVariable.getMediaDataFolder() + "_oid" + File.separator + finalPath
+ + File.separator;
+ try {
+ createFolder(filePath);
+ } catch (final IOException e) {
+ e.printStackTrace();
+ }
+ filePath += stringOid;
+ return filePath;
+ }
+
+ public static String getFileMetaData(final ObjectId oid) {
+ return getFileData(oid) + ".json";
+ }
+
+ public Data getWithSha512(final String sha512) {
+ LOGGER.info("find sha512 = {}", sha512);
+ try {
+ return DataAccess.getWhere(Data.class, new Condition(new QueryCondition("sha512", "=", sha512)));
+ } catch (final Exception e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ public Data getWithId(final long id) {
+ LOGGER.info("find id = {}", id);
+ try {
+ return DataAccess.get(Data.class, id);
+ } catch (final Exception e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ protected String getMimeType(final String extension) throws IOException {
+ return switch (extension.toLowerCase()) {
+ case "jpg", "jpeg" -> "image/jpeg";
+ case "png" -> "image/png";
+ case "webp" -> "image/webp";
+ case "mka" -> "audio/x-matroska";
+ case "mkv" -> "video/x-matroska";
+ case "webm" -> "video/webm";
+ default -> throw new IOException("Can not find the mime type of data input: '" + extension + "'");
+ };
+ }
+
+ public Data createNewData(final long tmpUID, final String originalFileName, final String sha512)
+ throws IOException {
+ // determine mime type:
+ Data injectedData = new Data();
+ String mimeType = "";
+ final String extension = originalFileName.substring(originalFileName.lastIndexOf('.') + 1);
+ mimeType = getMimeType(extension);
+ injectedData.mimeType = mimeType;
+ injectedData.sha512 = sha512;
+ final String tmpPath = getTmpFileInData(tmpUID);
+ injectedData.size = Files.size(Paths.get(tmpPath));
+
+ try {
+ injectedData = DataAccess.insert(injectedData);
+ } catch (final Exception e) {
+ e.printStackTrace();
+ return null;
+ }
+ final String mediaPath = getFileData(injectedData.oid);
+ LOGGER.info("src = {}", tmpPath);
+ LOGGER.info("dst = {}", mediaPath);
+ Files.move(Paths.get(tmpPath), Paths.get(mediaPath), StandardCopyOption.ATOMIC_MOVE);
+ LOGGER.info("Move done");
+ return injectedData;
+ }
+
+ public static void modeFileOldModelToNewModel(final UUID uuid, final ObjectId oid) throws IOException {
+ String mediaCurentPath = getFileDataOld(uuid);
+ String mediaDestPath = getFileData(oid);
+ LOGGER.info("src = {}", mediaCurentPath);
+ LOGGER.info("dst = {}", mediaDestPath);
+ if (Files.exists(Paths.get(mediaCurentPath))) {
+ LOGGER.info("move: {} ==> {}", mediaCurentPath, mediaDestPath);
+ Files.move(Paths.get(mediaCurentPath), Paths.get(mediaDestPath), StandardCopyOption.ATOMIC_MOVE);
+ }
+ // Move old meta-data...
+ mediaCurentPath = mediaCurentPath.substring(mediaCurentPath.length() - 4) + "meta.json";
+ mediaDestPath = mediaCurentPath.substring(mediaDestPath.length() - 4) + "meta.json";
+ if (Files.exists(Paths.get(mediaCurentPath))) {
+ LOGGER.info("moveM: {} ==> {}", mediaCurentPath, mediaDestPath);
+ Files.move(Paths.get(mediaCurentPath), Paths.get(mediaDestPath), StandardCopyOption.ATOMIC_MOVE);
+ }
+ LOGGER.info("Move done");
+ }
+
+ public static String saveTemporaryFile(final InputStream uploadedInputStream, final long idData)
+ throws FailException {
+ return saveFile(uploadedInputStream, DataResource.getTmpFileInData(idData));
+ }
+
+ public static void removeTemporaryFile(final long idData) {
+ final String filepath = DataResource.getTmpFileInData(idData);
+ if (Files.exists(Paths.get(filepath))) {
+ try {
+ Files.delete(Paths.get(filepath));
+ } catch (final IOException e) {
+ LOGGER.info("can not delete temporary file : {}", Paths.get(filepath));
+ e.printStackTrace();
+ }
+ }
+ }
+
+ // save uploaded file to a defined location on the server
+ static String saveFile(final InputStream uploadedInputStream, final String serverLocation) throws FailException {
+ String out = "";
+ MessageDigest md = null;
+ try (OutputStream outpuStream = new FileOutputStream(new File(serverLocation))) {
+ md = MessageDigest.getInstance("SHA-512");
+ outpuStream.flush();
+ } catch (final IOException ex) {
+ throw new FailException(Response.Status.INTERNAL_SERVER_ERROR, "Can not write in temporary file", ex);
+ } catch (final NoSuchAlgorithmException ex) {
+ throw new FailException(Response.Status.INTERNAL_SERVER_ERROR, "Can not find sha512 algorithms", ex);
+ }
+ if (md != null) {
+ try (OutputStream outpuStream = new FileOutputStream(new File(serverLocation))) {
+ int read = 0;
+ final byte[] bytes = new byte[CHUNK_SIZE_IN];
+ while ((read = uploadedInputStream.read(bytes)) != -1) {
+ // logger.info("write {}", read);
+ md.update(bytes, 0, read);
+ outpuStream.write(bytes, 0, read);
+ }
+ LOGGER.info("Flush input stream ... {}", serverLocation);
+ outpuStream.flush();
+ // create the end of sha512
+ final byte[] sha512Digest = md.digest();
+ // convert in hexadecimal
+ out = bytesToHex(sha512Digest);
+ uploadedInputStream.close();
+ } catch (final IOException ex) {
+ throw new FailException(Response.Status.INTERNAL_SERVER_ERROR, "Can not write in temporary file", ex);
+ }
+ }
+ return out;
+ }
+
+ public static String bytesToHex(final byte[] bytes) {
+ final StringBuilder sb = new StringBuilder();
+ for (final byte b : bytes) {
+ sb.append(String.format("%02x", b));
+ }
+ return sb.toString();
+ }
+
+ public Data getSmall(final ObjectId oid) {
+ try {
+ return DataAccess.get(Data.class, oid);
+ } catch (final Exception e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ @POST
+ @Path("/upload/")
+ @Consumes({ MediaType.MULTIPART_FORM_DATA })
+ @RolesAllowed("ADMIN")
+ @Operation(description = "Insert a new data in the data environment", tags = "SYSTEM")
+ public void uploadFile(
+ @Context final SecurityContext sc,
+ @FormDataParam("file") final InputStream fileInputStream,
+ @FormDataParam("file") final FormDataContentDisposition fileMetaData) throws FailException {
+ final GenericContext gc = (GenericContext) sc.getUserPrincipal();
+ LOGGER.info("===================================================");
+ LOGGER.info("== DATA uploadFile {}", (gc == null ? "null" : gc.userByToken));
+ LOGGER.info("===================================================");
+ // public NodeSmall uploadFile(final FormDataMultiPart form) {
+ LOGGER.info("Upload file: ");
+ final String filePath = ConfigBaseVariable.getTmpDataFolder() + File.separator + tmpFolderId++;
+ try {
+ createFolder(ConfigBaseVariable.getTmpDataFolder() + File.separator);
+ } catch (final IOException ex) {
+ throw new FailException(Response.Status.INTERNAL_SERVER_ERROR,
+ "Impossible to create the folder in the server", ex);
+ }
+ saveFile(fileInputStream, filePath);
+ }
+
+ @GET
+ @Path("{oid}")
+ @PermitTokenInURI
+ @RolesAllowed("USER")
+ @Produces(MediaType.APPLICATION_OCTET_STREAM)
+ @Operation(description = "Get back some data from the data environment", tags = "SYSTEM")
+ public Response retrieveDataId(
+ @Context final SecurityContext sc,
+ @QueryParam(HttpHeaders.AUTHORIZATION) final String token,
+ @HeaderParam("Range") final String range,
+ @PathParam("oid") final ObjectId oid) throws FailException {
+ final GenericContext gc = (GenericContext) sc.getUserPrincipal();
+ // logger.info("===================================================");
+ LOGGER.info("== DATA retrieveDataId ? oid={} user={}", oid, (gc == null ? "null" : gc.userByToken));
+ // logger.info("===================================================");
+ final Data value = getSmall(oid);
+ if (value == null) {
+ return Response.status(404).entity("media NOT FOUND: " + oid).type("text/plain").build();
+ }
+ try {
+ return buildStream(getFileData(oid), range,
+ value.mimeType == null ? "application/octet-stream" : value.mimeType);
+ } catch (final Exception ex) {
+ throw new FailException(Response.Status.INTERNAL_SERVER_ERROR, "Fail to build output stream", ex);
+ }
+ }
+
+ @GET
+ @Path("thumbnail/{oid}")
+ @RolesAllowed("USER")
+ @PermitTokenInURI
+ @Produces(MediaType.APPLICATION_OCTET_STREAM)
+ @Operation(description = "Get a thumbnail of from the data environment (if resize is possible)", tags = "SYSTEM")
+ // @CacheMaxAge(time = 10, unit = TimeUnit.DAYS)
+ public Response retrieveDataThumbnailId(
+ @Context final SecurityContext sc,
+ @QueryParam(HttpHeaders.AUTHORIZATION) final String token,
+ @HeaderParam("Range") final String range,
+ @PathParam("oid") final ObjectId oid) throws FailException {
+ final GenericContext gc = (GenericContext) sc.getUserPrincipal();
+ LOGGER.info("===================================================");
+ LOGGER.info("== DATA retrieveDataThumbnailId ? {}", (gc == null ? "null" : gc.userByToken));
+ LOGGER.info("===================================================");
+ final Data value = getSmall(oid);
+ if (value == null) {
+ return Response.status(404).entity("media NOT FOUND: " + oid).type("text/plain").build();
+ }
+ final String filePathName = getFileData(oid);
+ final File inputFile = new File(filePathName);
+ if (!inputFile.exists()) {
+ return Response.status(404).entity("{\"error\":\"media Does not exist: " + oid + "\"}")
+ .type("application/json").build();
+ }
+ if (value.mimeType.contentEquals("image/jpeg") || value.mimeType.contentEquals("image/png")
+ // || value.mimeType.contentEquals("image/webp")
+ ) {
+ // reads input image
+ BufferedImage inputImage;
+ try {
+ inputImage = ImageIO.read(inputFile);
+ } catch (final IOException ex) {
+ throw new FailException(Response.Status.INTERNAL_SERVER_ERROR, "Fail to READ the image", ex);
+ }
+ LOGGER.info("input size image: {}x{} type={}", inputImage.getWidth(), inputImage.getHeight(),
+ inputImage.getType());
+ final int scaledWidth = ConfigBaseVariable.getThumbnailWidth();
+ final int scaledHeight = (int) ((float) inputImage.getHeight() / (float) inputImage.getWidth()
+ * scaledWidth);
+ // creates output image
+ final BufferedImage outputImage = new BufferedImage(scaledWidth, scaledHeight, inputImage.getType());
+
+ // scales the input image to the output image
+ final Graphics2D g2d = outputImage.createGraphics();
+ LOGGER.info("output size image: {}x{}", scaledWidth, scaledHeight);
+ g2d.drawImage(inputImage, 0, 0, scaledWidth, scaledHeight, null);
+ g2d.dispose();
+ // create the output stream:
+ final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ try {
+ ImageIO.write(outputImage, ConfigBaseVariable.getThumbnailFormat(), baos);
+ } catch (final IOException e) {
+ e.printStackTrace();
+ return Response.status(500).entity("Internal Error: resize fail: " + e.getMessage()).type("text/plain")
+ .build();
+ }
+ final byte[] imageData = baos.toByteArray();
+ LOGGER.info("output length {}", imageData.length);
+ if (imageData.length == 0) {
+ LOGGER.error("Fail to convert image... Availlable format:");
+ for (final String data : ImageIO.getWriterFormatNames()) {
+ LOGGER.error(" - {}", data);
+ }
+ }
+ final Response.ResponseBuilder out = Response.ok(imageData).header(HttpHeaders.CONTENT_LENGTH,
+ imageData.length);
+ try {
+ out.type(getMimeType(ConfigBaseVariable.getThumbnailFormat()));
+ } catch (final IOException ex) {
+ throw new FailException(Response.Status.INTERNAL_SERVER_ERROR,
+ "Fail to convert mime type of " + ConfigBaseVariable.getThumbnailFormat(), ex);
+ }
+ // TODO: move this in a decorator !!!
+ final CacheControl cc = new CacheControl();
+ cc.setMaxAge(3600);
+ cc.setNoCache(false);
+ out.cacheControl(cc);
+ return out.build();
+ }
+ try {
+ return buildStream(filePathName, range, value.mimeType);
+ } catch (final Exception ex) {
+ throw new FailException(Response.Status.INTERNAL_SERVER_ERROR, "Fail to build output stream", ex);
+ }
+ }
+
+ @GET
+ @Path("{oid}/{name}")
+ @PermitTokenInURI
+ @RolesAllowed("USER")
+ @Produces(MediaType.APPLICATION_OCTET_STREAM)
+ @Operation(description = "Get back some data from the data environment (with a beautiful name (permit download with basic name)", tags = "SYSTEM")
+ public Response retrieveDataFull(
+ @Context final SecurityContext sc,
+ @QueryParam(HttpHeaders.AUTHORIZATION) final String token,
+ @ApiInputOptional @HeaderParam("Range") final String range,
+ @PathParam("oid") final ObjectId oid,
+ @PathParam("name") final String name) throws Exception {
+ final GenericContext gc = (GenericContext) sc.getUserPrincipal();
+ // logger.info("===================================================");
+ LOGGER.info("== DATA retrieveDataFull ? id={} user={}", oid, (gc == null ? "null" : gc.userByToken));
+ // logger.info("===================================================");
+ final Data value = getSmall(oid);
+ if (value == null) {
+ return Response.status(404).entity("media NOT FOUND: " + oid).type("text/plain").build();
+ }
+ return buildStream(getFileData(oid), range,
+ value.mimeType == null ? "application/octet-stream" : value.mimeType);
+ }
+
+ /** Adapted from http://stackoverflow.com/questions/12768812/video-streaming-to-ipad-does-not-work-with-tapestry5/12829541#12829541
+ *
+ * @param range range header
+ * @return Streaming output
+ * @throws FileNotFoundException
+ * @throws Exception IOException if an error occurs in streaming. */
+ private Response buildStream(final String filename, final String range, final String mimeType)
+ throws FailException {
+ final File file = new File(filename);
+ // logger.info("request range : {}", range);
+ // range not requested : Firefox does not send range headers
+ if (range == null) {
+ final StreamingOutput output = new StreamingOutput() {
+ @Override
+ public void write(final OutputStream out) {
+ try (FileInputStream in = new FileInputStream(file)) {
+ final byte[] buf = new byte[1024 * 1024];
+ int len;
+ while ((len = in.read(buf)) != -1) {
+ try {
+ out.write(buf, 0, len);
+ out.flush();
+ // logger.info("---- wrote {} bytes file ----", len);
+ } catch (final IOException ex) {
+ LOGGER.info("remote close connection");
+ break;
+ }
+ }
+ } catch (final IOException ex) {
+ throw new InternalServerErrorException(ex);
+ }
+ }
+ };
+ final Response.ResponseBuilder out = Response.ok(output).header(HttpHeaders.CONTENT_LENGTH, file.length());
+ if (mimeType != null) {
+ out.type(mimeType);
+ }
+ return out.build();
+
+ }
+
+ final String[] ranges = range.split("=")[1].split("-");
+ final long from = Long.parseLong(ranges[0]);
+
+ // logger.info("request range : {}", ranges.length);
+ // Chunk media if the range upper bound is unspecified. Chrome, Opera sends "bytes=0-"
+ long to = CHUNK_SIZE + from;
+ if (ranges.length == 1) {
+ to = file.length() - 1;
+ } else if (to >= file.length()) {
+ to = file.length() - 1;
+ }
+ final String responseRange = String.format("bytes %d-%d/%d", from, to, file.length());
+ // LOGGER.info("responseRange: {}", responseRange);
+ try {
+ final RandomAccessFile raf = new RandomAccessFile(file, "r");
+ raf.seek(from);
+
+ final long len = to - from + 1;
+ final MediaStreamer streamer = new MediaStreamer(len, raf);
+ final Response.ResponseBuilder out = Response.ok(streamer).status(Response.Status.PARTIAL_CONTENT)
+ .header("Accept-Ranges", "bytes").header("Content-Range", responseRange)
+ .header(HttpHeaders.CONTENT_LENGTH, streamer.getLenth())
+ .header(HttpHeaders.LAST_MODIFIED, new Date(file.lastModified()));
+ if (mimeType != null) {
+ out.type(mimeType);
+ }
+ return out.build();
+ } catch (final FileNotFoundException ex) {
+ throw new FailException(Response.Status.INTERNAL_SERVER_ERROR, "Fail to find the required file.", ex);
+ } catch (final IOException ex) {
+ throw new FailException(Response.Status.INTERNAL_SERVER_ERROR, "Fail to access to the required file.", ex);
+ }
+ }
+
+ public void undelete(final Long id) throws Exception {
+ DataAccess.unsetDelete(Data.class, id);
+ }
+
+}
diff --git a/back/src/org/atriasoft/karideo/api/UserMediaAdvancementResource.java b/back/src/org/atriasoft/karideo/api/UserMediaAdvancementResource.java
index 7e34232..d149c88 100644
--- a/back/src/org/atriasoft/karideo/api/UserMediaAdvancementResource.java
+++ b/back/src/org/atriasoft/karideo/api/UserMediaAdvancementResource.java
@@ -29,34 +29,43 @@ import jakarta.ws.rs.core.SecurityContext;
@Produces(MediaType.APPLICATION_JSON)
public class UserMediaAdvancementResource {
static final Logger LOGGER = LoggerFactory.getLogger(UserMediaAdvancementResource.class);
-
+
@GET
@Path("{id}")
@RolesAllowed("USER")
@Operation(description = "Get a specific user advancement with his ID", tags = "GLOBAL")
- public UserMediaAdvancement get(@Context final SecurityContext sc, @PathParam("id") final Long id) throws Exception {
+ public UserMediaAdvancement get(@Context final SecurityContext sc, @PathParam("id") final Long id)
+ throws Exception {
final GenericContext gc = (GenericContext) sc.getUserPrincipal();
- return DataAccess.getWhere(UserMediaAdvancement.class, new Condition(new QueryAnd(new QueryCondition("mediaId", "=", id), new QueryCondition("userId", "=", gc.userByToken.id))));
+ return DataAccess.getWhere(UserMediaAdvancement.class,
+ new Condition(new QueryAnd(new QueryCondition("mediaId", "=", id),
+ new QueryCondition("userId", "=", gc.userByToken.id))));
}
-
+
@GET
@RolesAllowed("USER")
@Operation(description = "Get all user advancement", tags = "GLOBAL")
public List gets(@Context final SecurityContext sc) throws Exception {
final GenericContext gc = (GenericContext) sc.getUserPrincipal();
- return DataAccess.getsWhere(UserMediaAdvancement.class, new Condition(new QueryCondition("userId", "=", gc.userByToken.id)));
+ return DataAccess.getsWhere(UserMediaAdvancement.class,
+ new Condition(new QueryCondition("userId", "=", gc.userByToken.id)));
}
-
+
/* ============================================================================= Modification SECTION: ============================================================================= */
-
- public record MediaInformations(int time, float percent, int count) {
- }
-
+
+ public record MediaInformations(
+ int time,
+ float percent,
+ int count) {}
+
// @POST
// @Path("{id}")
// @RolesAllowed("USER")
// @Consumes(MediaType.APPLICATION_JSON)
- public UserMediaAdvancement post(@Context final SecurityContext sc, @PathParam("id") final Long id, final MediaInformations data) throws Exception {
+ public UserMediaAdvancement post(
+ @Context final SecurityContext sc,
+ @PathParam("id") final Long id,
+ final MediaInformations data) throws Exception {
final GenericContext gc = (GenericContext) sc.getUserPrincipal();
final UserMediaAdvancement elem = new UserMediaAdvancement();
elem.userId = gc.userByToken.id;
@@ -66,16 +75,21 @@ public class UserMediaAdvancementResource {
elem.count = data.count;
return DataAccess.insert(elem);
}
-
- public record MediaInformationsDelta(int time, float percent, boolean addCount) {
- }
-
+
+ public record MediaInformationsDelta(
+ int time,
+ float percent,
+ boolean addCount) {}
+
@PUT
@Path("{id}")
@RolesAllowed("USER")
@Consumes(MediaType.APPLICATION_JSON)
@Operation(description = "Modify a user advancement", tags = "GLOBAL")
- public UserMediaAdvancement patch(@Context final SecurityContext sc, @PathParam("id") final Long id, @Valid final MediaInformationsDelta data) throws Exception {
+ public UserMediaAdvancement patch(
+ @Context final SecurityContext sc,
+ @PathParam("id") final Long id,
+ @Valid final MediaInformationsDelta data) throws Exception {
final UserMediaAdvancement elem = get(sc, id);
if (elem == null) {
// insert element
@@ -91,10 +105,10 @@ public class UserMediaAdvancementResource {
elem.count++;
}
LOGGER.info("{},{},{}", elem.time, elem.percent, elem.count);
- final long nbAfected = DataAccess.update(elem, elem.id, List.of("time", "percent", "count"));
+ DataAccess.update(elem, elem.id, List.of("time", "percent", "count"));
return DataAccess.get(UserMediaAdvancement.class, elem.id);
}
-
+
@DELETE
@Path("{id}")
@RolesAllowed("USER")
@@ -103,5 +117,5 @@ public class UserMediaAdvancementResource {
final UserMediaAdvancement elem = get(sc, id);
DataAccess.delete(UserMediaAdvancement.class, elem.id);
}
-
+
}
diff --git a/back/test/src/test/atriasoft/karideo/ConfigureDb.java b/back/test/src/test/atriasoft/karideo/ConfigureDb.java
index 40b526f..6b30ef4 100644
--- a/back/test/src/test/atriasoft/karideo/ConfigureDb.java
+++ b/back/test/src/test/atriasoft/karideo/ConfigureDb.java
@@ -1,18 +1,12 @@
package test.atriasoft.karideo;
import java.io.IOException;
-import java.util.List;
import org.atriasoft.archidata.dataAccess.DBAccess;
import org.atriasoft.archidata.db.DbConfig;
import org.atriasoft.archidata.db.DbIoFactory;
import org.atriasoft.archidata.exception.DataAccessException;
import org.atriasoft.archidata.tools.ConfigBaseVariable;
-import org.atriasoft.karideo.model.Media;
-import org.atriasoft.karideo.model.Season;
-import org.atriasoft.karideo.model.Series;
-import org.atriasoft.karideo.model.Type;
-import org.atriasoft.karideo.model.UserKarideo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -22,7 +16,7 @@ public class ConfigureDb {
final static private Logger LOGGER = LoggerFactory.getLogger(ConfigureDb.class);
final static private String modeTestForced = null;// "MONGO";
public static DBAccess da = null;
-
+
public static void configure() throws IOException, InternalServerErrorException, DataAccessException {
String modeTest = System.getenv("TEST_E2E_MODE");
if (modeTest == null || modeTest.isEmpty() || "false".equalsIgnoreCase(modeTest)) {
@@ -38,13 +32,6 @@ public class ConfigureDb {
ConfigBaseVariable.apiAdress = "http://127.0.0.1:12342/test/api/";
// Enable the test mode permit to access to the test token (never use it in production).
ConfigBaseVariable.testMode = "true";
- final List> listObject = List.of( //
- Media.class, //
- Season.class, //
- Series.class, //
- Type.class, //
- UserKarideo.class //
- );
if ("SQLITE-MEMORY".equalsIgnoreCase(modeTest)) {
ConfigBaseVariable.dbType = "sqlite";
ConfigBaseVariable.bdDatabase = null;
@@ -73,7 +60,7 @@ public class ConfigureDb {
// Connect the dataBase...
da = DBAccess.createInterface();
}
-
+
public static void removeDB() {
String modeTest = System.getenv("TEST_E2E_MODE");
if (modeTest == null || modeTest.isEmpty() || "false".equalsIgnoreCase(modeTest)) {
@@ -116,7 +103,7 @@ public class ConfigureDb {
return;
}
}
-
+
public static void clear() throws IOException {
LOGGER.info("Remove the test db");
removeDB();
diff --git a/back/test/src/test/atriasoft/karideo/TestHealthCheck.java b/back/test/src/test/atriasoft/karideo/TestHealthCheck.java
index f15faf7..0c4ade2 100644
--- a/back/test/src/test/atriasoft/karideo/TestHealthCheck.java
+++ b/back/test/src/test/atriasoft/karideo/TestHealthCheck.java
@@ -1,5 +1,9 @@
package test.atriasoft.karideo;
+import org.atriasoft.archidata.exception.RESTErrorResponseException;
+import org.atriasoft.archidata.tools.ConfigBaseVariable;
+import org.atriasoft.archidata.tools.RESTApi;
+import org.atriasoft.karideo.api.HealthCheck.HealthResult;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
@@ -8,10 +12,6 @@ import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.junit.jupiter.api.extension.ExtendWith;
-import org.atriasoft.archidata.exception.RESTErrorResponseException;
-import org.atriasoft.archidata.tools.ConfigBaseVariable;
-import org.atriasoft.archidata.tools.RESTApi;
-import org.atriasoft.karideo.api.HealthCheck.HealthResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -47,14 +47,15 @@ public class TestHealthCheck {
@Test
// @RepeatedTest(10)
public void checkHealthCheck() throws Exception {
- final HealthResult result = api.get(HealthResult.class, "health_check");
+ final HealthResult result = api.request("health_check").get().fetch(HealthResult.class);
Assertions.assertEquals(result.value(), "alive and kicking");
}
@Order(2)
@Test
public void checkHealthCheckWrongAPI() throws Exception {
- Assertions.assertThrows(RESTErrorResponseException.class, () -> api.get(HealthResult.class, "health_checks"));
+ Assertions.assertThrows(RESTErrorResponseException.class,
+ () -> api.request("health_check_kaboom").get().fetch());
}
}
diff --git a/front/src/components/VideoPlayer.tsx b/front/src/components/VideoPlayer.tsx
index e23b07b..0a7f71c 100644
--- a/front/src/components/VideoPlayer.tsx
+++ b/front/src/components/VideoPlayer.tsx
@@ -168,7 +168,8 @@ export const VideoPlayer = ({}: AudioPlayerProps) => {
}
}, [isPlaying, videoRef]);
- const onAudioEnded = () => {
+ const onEnded = () => {
+ console.log('ended ...');
if (playMediaList.length === 0 || isNullOrUndefined(MediaOffset)) {
return;
}
@@ -193,6 +194,7 @@ export const VideoPlayer = ({}: AudioPlayerProps) => {
videoRef.current.currentTime = newValue;
};
const onPlay = () => {
+ console.log(`onPlay ...`);
if (!videoRef || !videoRef.current) {
return;
}
@@ -203,6 +205,7 @@ export const VideoPlayer = ({}: AudioPlayerProps) => {
}
};
const onStop = () => {
+ console.log(`onStop ...`);
if (!videoRef || !videoRef.current) {
return;
}
@@ -310,6 +313,36 @@ export const VideoPlayer = ({}: AudioPlayerProps) => {
}
}
};
+ const onError = (event) => {
+ const errorDetails: MediaError = event.target.error;
+ let errorMessage = 'An error occurred while trying to play the video.';
+
+ if (errorDetails) {
+ if (errorDetails.code === 1) {
+ errorMessage =
+ 'The video cannot be played because the source is invalid or the format is unsupported.';
+ } else if (errorDetails.code === 2) {
+ errorMessage =
+ 'The video cannot be played because the network error occurred.';
+ } else if (errorDetails.code === 3) {
+ errorMessage =
+ 'The video cannot be played because of a decoding error.';
+ } else if (errorDetails.code === 4) {
+ errorMessage =
+ 'The video cannot be played because of an unknown error.';
+ }
+ }
+
+ //setError(errorMessage); // Mettre à jour l'état de l'erreur
+ console.error(
+ 'Error code:',
+ errorDetails.code,
+ ' ==> ',
+ errorMessage,
+ ' // ',
+ errorDetails.message
+ );
+ };
const [isFullScreen, setIsFullScreen] = useState(false);
useEffect(() => {
const handleFullScreenChange = () => {
@@ -351,10 +384,6 @@ export const VideoPlayer = ({}: AudioPlayerProps) => {
height={isFullScreen ? '100%' : undefined}
width={isFullScreen ? '100%' : undefined}
marginX="auto"
- // left={0}
- // right={0}
- // top={0}
- // bottom={0}
>