archidata/src/org/kar/archidata/api/DataResource.java

447 lines
17 KiB
Java

package org.kar.archidata.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.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.glassfish.jersey.media.multipart.FormDataContentDisposition;
import org.glassfish.jersey.media.multipart.FormDataParam;
import org.kar.archidata.annotation.security.PermitTokenInURI;
import org.kar.archidata.dataAccess.DataAccess;
import org.kar.archidata.dataAccess.QueryCondition;
import org.kar.archidata.dataAccess.options.Condition;
import org.kar.archidata.filter.GenericContext;
import org.kar.archidata.model.Data;
import org.kar.archidata.tools.ConfigBaseVariable;
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 long tmpFolderId) {
final String filePath = ConfigBaseVariable.getMediaDataFolder() + File.separator + tmpFolderId + File.separator + "data";
try {
createFolder(ConfigBaseVariable.getMediaDataFolder() + File.separator + tmpFolderId + File.separator);
} catch (final IOException e) {
e.printStackTrace();
}
return filePath;
}
public static String getFileData(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 getFileMetaData(final UUID uuid) {
return getFileData(uuid) + ".json";
}
public static 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 static 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;
}
public static 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 = 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 + "'");
};
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.id);
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 long id, final UUID uuid) throws IOException {
String mediaCurentPath = getFileDataOld(id);
String mediaDestPath = getFileData(uuid);
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) {
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) {
String out = "";
try {
OutputStream outpuStream = new FileOutputStream(new File(serverLocation));
int read = 0;
final byte[] bytes = new byte[CHUNK_SIZE_IN];
final MessageDigest md = MessageDigest.getInstance("SHA-512");
outpuStream = new FileOutputStream(new File(serverLocation));
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);
System.out.flush();
outpuStream.flush();
outpuStream.close();
// create the end of sha512
final byte[] sha512Digest = md.digest();
// convert in hexadecimal
out = bytesToHex(sha512Digest);
uploadedInputStream.close();
} catch (final IOException ex) {
LOGGER.info("Can not write in temporary file ... ");
ex.printStackTrace();
} catch (final NoSuchAlgorithmException ex) {
LOGGER.info("Can not find sha512 algorithms");
ex.printStackTrace();
}
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 UUID id) {
try {
return DataAccess.get(Data.class, id);
} 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) {
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 e) {
e.printStackTrace();
}
saveFile(fileInputStream, filePath);
}
@GET
@Path("{id}")
@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("id") final UUID id) throws Exception {
final GenericContext gc = (GenericContext) sc.getUserPrincipal();
// logger.info("===================================================");
LOGGER.info("== DATA retrieveDataId ? id={} user={}", id, (gc == null ? "null" : gc.userByToken));
// logger.info("===================================================");
final Data value = getSmall(id);
if (value == null) {
Response.status(404).entity("media NOT FOUND: " + id).type("text/plain").build();
}
return buildStream(getFileData(id), range, value.mimeType);
}
@GET
@Path("thumbnail/{id}")
@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("id") final UUID id) throws Exception {
// GenericContext gc = (GenericContext) sc.getUserPrincipal();
// logger.info("===================================================");
// logger.info("== DATA retrieveDataThumbnailId ? {}", (gc==null?"null":gc.user));
// logger.info("===================================================");
final Data value = getSmall(id);
if (value == null) {
return Response.status(404).entity("media NOT FOUND: " + id).type("text/plain").build();
}
final String filePathName = getFileData(id);
final File inputFile = new File(filePathName);
if (!inputFile.exists()) {
return Response.status(404).entity("{\"error\":\"media Does not exist: " + id + "\"}").type("application/json").build();
}
if (value.mimeType.contentEquals("image/jpeg") || value.mimeType.contentEquals("image/png")
// || value.mimeType.contentEquals("image/webp")
) {
// reads input image
final BufferedImage inputImage = ImageIO.read(inputFile);
final int scaledWidth = 250;
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();
g2d.drawImage(inputImage, 0, 0, scaledWidth, scaledHeight, null);
g2d.dispose();
// create the output stream:
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
// TODO: check how to remove buffer file !!! here, it is not needed at all...
ImageIO.write(outputImage, "JPG", 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();
// Response.ok(new ByteArrayInputStream(imageData)).build();
final Response.ResponseBuilder out = Response.ok(imageData).header(HttpHeaders.CONTENT_LENGTH, imageData.length);
out.type("image/jpeg");
// TODO: move this in a decorator !!!
final CacheControl cc = new CacheControl();
cc.setMaxAge(3600);
cc.setNoCache(false);
out.cacheControl(cc);
return out.build();
}
return buildStream(filePathName, range, value.mimeType);
}
@GET
@Path("{id}/{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, @HeaderParam("Range") final String range,
@PathParam("id") final UUID id, @PathParam("name") final String name) throws Exception {
final GenericContext gc = (GenericContext) sc.getUserPrincipal();
// logger.info("===================================================");
LOGGER.info("== DATA retrieveDataFull ? id={} user={}", id, (gc == null ? "null" : gc.userByToken));
// logger.info("===================================================");
final Data value = getSmall(id);
if (value == null) {
Response.status(404).entity("media NOT FOUND: " + id).type("text/plain").build();
}
return buildStream(getFileData(id), range, 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 Exception IOException if an error occurs in streaming. */
private Response buildStream(final String filename, final String range, final String mimeType) throws Exception {
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);
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();
}
public static void undelete(final Long id) throws Exception {
DataAccess.unsetDelete(Data.class, id);
}
}