diff --git a/pom.xml b/pom.xml
index 0bf5c06..4eaead2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -42,6 +42,10 @@
io.quarkus
quarkus-resteasy-jackson
+
+ io.quarkus
+ quarkus-resteasy-multipart
+
io.quarkus
quarkus-config-yaml
diff --git a/src/main/java/sh/rhiobet/lalafin/api/UploadPrivateAPI.java b/src/main/java/sh/rhiobet/lalafin/api/UploadPrivateAPI.java
new file mode 100644
index 0000000..dbfe9a1
--- /dev/null
+++ b/src/main/java/sh/rhiobet/lalafin/api/UploadPrivateAPI.java
@@ -0,0 +1,47 @@
+package sh.rhiobet.lalafin.api;
+
+import javax.inject.Inject;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import org.jboss.resteasy.annotations.jaxrs.PathParam;
+import org.jboss.resteasy.annotations.providers.multipart.MultipartForm;
+import io.quarkus.security.Authenticated;
+import io.quarkus.security.identity.SecurityIdentity;
+import sh.rhiobet.lalafin.api.model.UploadForm;
+import sh.rhiobet.lalafin.upload.UploadService;
+
+@Authenticated
+@Path("/api/private/upload")
+public class UploadPrivateAPI {
+ @Inject
+ UploadService uploadService;
+
+ @Inject
+ SecurityIdentity securityIdentity;
+
+ @POST
+ @Consumes(MediaType.MULTIPART_FORM_DATA)
+ @Produces(MediaType.TEXT_PLAIN)
+ public Response uploadFile(@MultipartForm UploadForm data) {
+ return uploadService.upload(securityIdentity.getPrincipal().getName(), data.filename,
+ data.password, data.file);
+ }
+
+ @GET
+ @Produces(MediaType.APPLICATION_JSON)
+ public Response listUploads() {
+ return uploadService.list(securityIdentity.getPrincipal().getName());
+ }
+
+ @DELETE
+ @Path("/{iv}")
+ public Response getFile(@PathParam String iv) {
+ return uploadService.delete(securityIdentity.getPrincipal().getName(), iv);
+ }
+}
diff --git a/src/main/java/sh/rhiobet/lalafin/api/UploadPublicAPI.java b/src/main/java/sh/rhiobet/lalafin/api/UploadPublicAPI.java
new file mode 100644
index 0000000..2c18b2f
--- /dev/null
+++ b/src/main/java/sh/rhiobet/lalafin/api/UploadPublicAPI.java
@@ -0,0 +1,25 @@
+package sh.rhiobet.lalafin.api;
+
+import javax.inject.Inject;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Response;
+import org.jboss.resteasy.annotations.jaxrs.PathParam;
+import sh.rhiobet.lalafin.upload.UploadService;
+
+@Path("/api/public/upload")
+public class UploadPublicAPI {
+ @Inject
+ UploadService uploadService;
+
+ @GET
+ @Path("/{user}/{iv}")
+ public Response getFile(@PathParam String user, @PathParam String iv,
+ @QueryParam("password") String password) {
+ if (password == null) {
+ password = "";
+ }
+ return uploadService.download(user, iv, password);
+ }
+}
diff --git a/src/main/java/sh/rhiobet/lalafin/api/model/UploadForm.java b/src/main/java/sh/rhiobet/lalafin/api/model/UploadForm.java
new file mode 100644
index 0000000..3b50117
--- /dev/null
+++ b/src/main/java/sh/rhiobet/lalafin/api/model/UploadForm.java
@@ -0,0 +1,22 @@
+package sh.rhiobet.lalafin.api.model;
+
+import java.io.InputStream;
+import javax.ws.rs.FormParam;
+import javax.ws.rs.core.MediaType;
+import org.jboss.resteasy.annotations.providers.multipart.PartType;
+
+public class UploadForm {
+
+ @FormParam("filename")
+ @PartType(MediaType.TEXT_PLAIN)
+ public String filename;
+
+ @FormParam("file")
+ @PartType(MediaType.APPLICATION_OCTET_STREAM)
+ public InputStream file;
+
+ @FormParam("password")
+ @PartType(MediaType.TEXT_PLAIN)
+ public String password;
+
+}
diff --git a/src/main/java/sh/rhiobet/lalafin/api/model/UploadedFile.java b/src/main/java/sh/rhiobet/lalafin/api/model/UploadedFile.java
new file mode 100644
index 0000000..c1411a2
--- /dev/null
+++ b/src/main/java/sh/rhiobet/lalafin/api/model/UploadedFile.java
@@ -0,0 +1,12 @@
+package sh.rhiobet.lalafin.api.model;
+
+import io.quarkus.runtime.annotations.RegisterForReflection;
+
+@RegisterForReflection
+public class UploadedFile {
+
+ public String name;
+ public String iv;
+ public String downloadUrl;
+
+}
diff --git a/src/main/java/sh/rhiobet/lalafin/file/FileHelper.java b/src/main/java/sh/rhiobet/lalafin/file/FileHelper.java
new file mode 100644
index 0000000..26f5b11
--- /dev/null
+++ b/src/main/java/sh/rhiobet/lalafin/file/FileHelper.java
@@ -0,0 +1,44 @@
+package sh.rhiobet.lalafin.file;
+
+public class FileHelper {
+
+ public static String getMimeType(String filename) {
+ String extension = filename.substring(filename.lastIndexOf('.') + 1);
+
+ switch (extension) {
+ case "3gp":
+ return "video/3gpp";
+ case "avi":
+ return "video/x-msvideo";
+ case "flac":
+ return "audio/x-flac";
+ case "flv":
+ return "video/x-flv";
+ case "html":
+ return "text/html";
+ case "jpg":
+ return "image/jpeg";
+ case "mkv":
+ return "video/x-matroska";
+ case "mp3":
+ return "audio/mp3";
+ case "mp4":
+ return "video/mp4";
+ case "png":
+ return "image/png";
+ case "ts":
+ return "video/MP2T";
+ case "wav":
+ return "audio/x-wav";
+ case "webm":
+ return "video/webm";
+ case "wmv":
+ return "video/x-ms-wmv";
+ case "zip":
+ return "application/zip";
+ default:
+ return "application/octet-stream";
+ }
+ }
+
+}
diff --git a/src/main/java/sh/rhiobet/lalafin/file/FileServeService.java b/src/main/java/sh/rhiobet/lalafin/file/FileServeService.java
index 6edc9f3..b644e3f 100644
--- a/src/main/java/sh/rhiobet/lalafin/file/FileServeService.java
+++ b/src/main/java/sh/rhiobet/lalafin/file/FileServeService.java
@@ -62,7 +62,7 @@ public class FileServeService {
response.header("Content-Range",
"bytes " + rangeStart + "-" + fileSize + "/" + fileSize);
}
- response.header("Content-Type", this.getMimeType(fileInfo.filename));
+ response.header("Content-Type", FileHelper.getMimeType(fileInfo.filename));
if (path.toString().contains("/.thumbnails/")) {
response.header("Cache-Control", "max-age=604800");
}
@@ -72,43 +72,4 @@ public class FileServeService {
}
}
- private String getMimeType(String filename) {
- String extension = filename.substring(filename.lastIndexOf('.') + 1);
-
- switch (extension) {
- case "3gp":
- return "video/3gpp";
- case "avi":
- return "video/x-msvideo";
- case "flac":
- return "audio/x-flac";
- case "flv":
- return "video/x-flv";
- case "html":
- return "text/html";
- case "jpg":
- return "image/jpeg";
- case "mkv":
- return "video/x-matroska";
- case "mp3":
- return "audio/mp3";
- case "mp4":
- return "video/mp4";
- case "png":
- return "image/png";
- case "ts":
- return "video/MP2T";
- case "wav":
- return "audio/x-wav";
- case "webm":
- return "video/webm";
- case "wmv":
- return "video/x-ms-wmv";
- case "zip":
- return "application/zip";
- default:
- return "application/octet-stream";
- }
- }
-
}
diff --git a/src/main/java/sh/rhiobet/lalafin/upload/UploadResource.java b/src/main/java/sh/rhiobet/lalafin/upload/UploadResource.java
new file mode 100644
index 0000000..a37a560
--- /dev/null
+++ b/src/main/java/sh/rhiobet/lalafin/upload/UploadResource.java
@@ -0,0 +1,25 @@
+package sh.rhiobet.lalafin.upload;
+
+import javax.inject.Inject;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.core.Response;
+import io.quarkus.security.Authenticated;
+import io.quarkus.security.identity.SecurityIdentity;
+
+@Authenticated
+@Path("/upload")
+public class UploadResource {
+ @Inject
+ SecurityIdentity securityIdentity;
+
+ @Inject
+ UploadService fileUploadService;
+
+ @GET
+ @Path("/")
+ public Response serveIndex() {
+ return fileUploadService.index(securityIdentity.getPrincipal().getName());
+ }
+
+}
diff --git a/src/main/java/sh/rhiobet/lalafin/upload/UploadService.java b/src/main/java/sh/rhiobet/lalafin/upload/UploadService.java
new file mode 100644
index 0000000..462b293
--- /dev/null
+++ b/src/main/java/sh/rhiobet/lalafin/upload/UploadService.java
@@ -0,0 +1,195 @@
+package sh.rhiobet.lalafin.upload;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.channels.Channels;
+import java.nio.channels.FileChannel;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.Key;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Optional;
+import javax.crypto.Cipher;
+import javax.crypto.CipherInputStream;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import javax.enterprise.context.ApplicationScoped;
+import javax.inject.Inject;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.ResponseBuilder;
+import javax.xml.bind.DatatypeConverter;
+import io.quarkus.qute.Location;
+import io.quarkus.qute.Template;
+import io.quarkus.qute.TemplateInstance;
+import sh.rhiobet.lalafin.api.configuration.FileApiConfiguration;
+import sh.rhiobet.lalafin.api.model.UploadedFile;
+import sh.rhiobet.lalafin.file.FileHelper;
+
+@ApplicationScoped
+public class UploadService {
+
+ @Inject
+ FileApiConfiguration fileApiConfiguration;
+
+ @Location("upload-index.html")
+ Template uploadTemplate;
+
+ public Response upload(String user, String filename, String passphrase,
+ InputStream inputStream) {
+ SecureRandom random = new SecureRandom();
+ byte ivBytes[] = new byte[16];
+ random.nextBytes(ivBytes);
+ String ivString = DatatypeConverter.printHexBinary(ivBytes);
+ Path filePath = Paths.get(fileApiConfiguration.directory() + "/upload/" + user + "/"
+ + ivString + "/" + filename);
+
+ try {
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ digest.update(ivBytes);
+ byte[] passphraseHash = digest.digest(passphrase.getBytes(StandardCharsets.UTF_8));
+ Key aesKey = new SecretKeySpec(passphraseHash, "AES");
+ IvParameterSpec iv = new IvParameterSpec(ivBytes);
+ Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+ cipher.init(Cipher.ENCRYPT_MODE, aesKey, iv);
+ CipherInputStream cipherInputStream = new CipherInputStream(inputStream, cipher);
+ filePath.getParent().toFile().mkdirs();
+ Files.copy(cipherInputStream, filePath);
+
+ digest = MessageDigest.getInstance("SHA-256");
+ digest.update(user.getBytes(StandardCharsets.UTF_8));
+ byte[] keyHash = digest.digest(passphraseHash);
+ Files.writeString(filePath.getParent().resolve(".key"),
+ DatatypeConverter.printHexBinary(keyHash));
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
+ | IOException | InvalidAlgorithmParameterException e) {
+ e.printStackTrace();
+ return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
+ }
+
+ return Response
+ .ok("/api/public/upload/" + user + "/" + ivString + "?password=" + passphrase)
+ .build();
+ }
+
+ public Response list(String user) {
+ Path folderPath = Paths.get(fileApiConfiguration.directory() + "/upload/" + user);
+ if (!Files.exists(folderPath)) {
+ folderPath.toFile().mkdirs();
+ }
+
+ List uploadedFiles = new ArrayList<>();
+ try {
+ Files.list(folderPath).forEach(p -> {
+ final UploadedFile uploadedFile = new UploadedFile();
+ uploadedFile.iv = p.getFileName().toString();
+ uploadedFile.downloadUrl = "/api/public/upload/" + user + "/" + uploadedFile.iv;
+ Optional filePathOptional;
+ try {
+ filePathOptional = Files.list(p)
+ .filter(p2 -> !p2.getFileName().toString().equals(".key")).findFirst();
+ if (filePathOptional.isPresent()) {
+ uploadedFile.name = filePathOptional.get().getFileName().toString();
+ uploadedFiles.add(uploadedFile);
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ });
+ return Response.ok(uploadedFiles).build();
+ } catch (IOException e) {
+ e.printStackTrace();
+ return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
+ }
+ }
+
+ public Response index(String user) {
+ TemplateInstance uploadTemplateInstance = uploadTemplate.instance().data("user", user);
+
+ ResponseBuilder response = Response.ok(uploadTemplateInstance.render());
+ response.header("Content-Type", "text/html");
+ return response.build();
+ }
+
+ public Response download(String user, String ivString, String password) {
+ Path folderPath =
+ Paths.get(fileApiConfiguration.directory() + "/upload/" + user + "/" + ivString);
+ byte ivBytes[] = DatatypeConverter.parseHexBinary(ivString);
+
+ if (!Files.exists(folderPath)) {
+ return Response.status(Response.Status.NOT_FOUND).build();
+ }
+
+ try {
+ Optional filePathOptional = Files.list(folderPath)
+ .filter(p -> !p.getFileName().toString().equals(".key")).findFirst();
+ if (!filePathOptional.isPresent()) {
+ return Response.status(Response.Status.NOT_FOUND).build();
+ }
+ Path filePath = filePathOptional.get();
+ Path keyPath = filePath.getParent().resolve(".key");
+ String keyHashString = Files.readString(keyPath);
+
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ digest.update(ivBytes);
+ byte[] passphraseHash = digest.digest(password.getBytes(StandardCharsets.UTF_8));
+
+ digest = MessageDigest.getInstance("SHA-256");
+ digest.update(user.getBytes(StandardCharsets.UTF_8));
+ byte[] keyHash = digest.digest(passphraseHash);
+ if (!keyHashString.equals(DatatypeConverter.printHexBinary(keyHash))) {
+ return Response.status(Response.Status.NOT_ACCEPTABLE).entity("Wrong password")
+ .build();
+ }
+
+ Key aesKey = new SecretKeySpec(passphraseHash, "AES");
+ IvParameterSpec iv = new IvParameterSpec(ivBytes);
+ Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+ cipher.init(Cipher.DECRYPT_MODE, aesKey, iv);
+
+ FileChannel channel = FileChannel.open(filePath);
+ InputStream is = Channels.newInputStream(channel);
+ CipherInputStream cipherInputStream = new CipherInputStream(is, cipher);
+
+ ResponseBuilder response = Response.ok(filePath.toFile());
+ response.entity(cipherInputStream);
+ response.header("Accept-Ranges", "bytes");
+ response.header("Content-Disposition",
+ "inline; filename=\"" + filePath.getFileName().toString() + "\"");
+ response.header("Content-Type",
+ FileHelper.getMimeType(filePath.getFileName().toString()));
+ return response.build();
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
+ | IOException | InvalidAlgorithmParameterException e) {
+ e.printStackTrace();
+ return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
+ }
+ }
+
+ public Response delete(String user, String ivString) {
+ Path folderPath =
+ Paths.get(fileApiConfiguration.directory() + "/upload/" + user + "/" + ivString);
+ if (!Files.exists(folderPath)) {
+ return Response.status(Response.Status.NOT_FOUND).build();
+ }
+ try {
+ Files.walk(folderPath).sorted(Comparator.reverseOrder()).map(Path::toFile)
+ .forEach(File::delete);
+ return Response.ok().build();
+ } catch (IOException e) {
+ e.printStackTrace();
+ return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
+ }
+ }
+
+}
diff --git a/src/main/resources/application.yaml.example b/src/main/resources/application.yaml.example
index ac0e32e..422e07f 100644
--- a/src/main/resources/application.yaml.example
+++ b/src/main/resources/application.yaml.example
@@ -1,6 +1,8 @@
# Configuration file
quarkus:
http:
+ limits:
+ max-body-size: 50G
port: 8910
proxy:
proxy-address-forwarding: true
diff --git a/src/main/resources/templates/upload-index.html b/src/main/resources/templates/upload-index.html
new file mode 100644
index 0000000..ac0cfb2
--- /dev/null
+++ b/src/main/resources/templates/upload-index.html
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+ {user}'s uploads
+
+
+
+ {user}'s uploads
+
+
+
+
+
+ History
+
+
+
+
+
\ No newline at end of file