From 5e453dad51ba8c7b615273befefc5e62d78e46f7 Mon Sep 17 00:00:00 2001 From: RhiobeT Date: Tue, 21 Apr 2026 17:28:53 +0200 Subject: [PATCH] Partially reimplement private file API --- .../rhiobet/lalafin/LalafinConfiguration.java | 2 +- .../lalafin/advent/AdventConfiguration.java | 2 +- .../advent/AdventThumbnailPathPlugin.java | 67 +++++++++++++ .../lalafin/file/FileConfiguration.java | 2 +- .../lalafin/file/FileMetadataService.java | 94 +++++++++++++++++++ .../FileThumbnailPathPlugin.java} | 40 ++++---- .../lalafin/file/rest/FilePrivateAPI.java | 63 +++++++++++++ .../rhiobet/lalafin/path/FileSystemPath.java | 13 ++- 8 files changed, 255 insertions(+), 28 deletions(-) create mode 100644 src/main/java/sh/rhiobet/lalafin/advent/AdventThumbnailPathPlugin.java create mode 100644 src/main/java/sh/rhiobet/lalafin/file/FileMetadataService.java rename src/main/java/sh/rhiobet/lalafin/{thumbnail/ThumbnailPathPlugin.java => file/FileThumbnailPathPlugin.java} (71%) create mode 100644 src/main/java/sh/rhiobet/lalafin/file/rest/FilePrivateAPI.java diff --git a/src/main/java/sh/rhiobet/lalafin/LalafinConfiguration.java b/src/main/java/sh/rhiobet/lalafin/LalafinConfiguration.java index ad22ed2..1df2bad 100644 --- a/src/main/java/sh/rhiobet/lalafin/LalafinConfiguration.java +++ b/src/main/java/sh/rhiobet/lalafin/LalafinConfiguration.java @@ -4,7 +4,7 @@ import java.util.List; import java.util.Optional; import io.smallrye.config.ConfigMapping; -@ConfigMapping(prefix = "lalafin") +@ConfigMapping(prefix = "lalafin.core") public interface LalafinConfiguration { public String base_url(); diff --git a/src/main/java/sh/rhiobet/lalafin/advent/AdventConfiguration.java b/src/main/java/sh/rhiobet/lalafin/advent/AdventConfiguration.java index 817f478..064fc48 100644 --- a/src/main/java/sh/rhiobet/lalafin/advent/AdventConfiguration.java +++ b/src/main/java/sh/rhiobet/lalafin/advent/AdventConfiguration.java @@ -3,7 +3,7 @@ package sh.rhiobet.lalafin.advent; import java.util.List; import io.smallrye.config.ConfigMapping; -@ConfigMapping(prefix = "advent") +@ConfigMapping(prefix = "lalafin.advent") public interface AdventConfiguration { public List events(); diff --git a/src/main/java/sh/rhiobet/lalafin/advent/AdventThumbnailPathPlugin.java b/src/main/java/sh/rhiobet/lalafin/advent/AdventThumbnailPathPlugin.java new file mode 100644 index 0000000..234e34c --- /dev/null +++ b/src/main/java/sh/rhiobet/lalafin/advent/AdventThumbnailPathPlugin.java @@ -0,0 +1,67 @@ +package sh.rhiobet.lalafin.advent; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Optional; + +import jakarta.decorator.Decorator; +import jakarta.decorator.Delegate; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import sh.rhiobet.lalafin.api.configuration.FileApiConfiguration; +import sh.rhiobet.lalafin.path.Path; +import sh.rhiobet.lalafin.path.PathAccessor; +import sh.rhiobet.lalafin.path.PathPlugin; + +@Decorator +public class AdventThumbnailPathPlugin extends PathAccessor implements PathPlugin { + @Inject + FileApiConfiguration fileApiConfiguration; + + @Inject + @Named("file/thumbnail") + @Delegate + PathPlugin thumbnailPathPlugin; + + @Inject + AdventAccessService adventAccessService; + + @Override + public Optional resolveURI(Path path) { + if (this.adventAccessService.checkAccess(path)) { + return this.thumbnailPathPlugin.resolveURI(path); + } else { + Optional thumbnailAbsolutePath = getDefaultThumbnailPath(path); + + if (thumbnailAbsolutePath.isPresent()) { + java.nio.file.Path rootFolderPath = Paths.get(this.fileApiConfiguration.directory()); + return Optional.of( + "/" + rootFolderPath.resolve("file").relativize(thumbnailAbsolutePath.get()) + .toString()); + } else { + return Optional.empty(); + } + } + } + + @Override + public OutputStream getOutputStream(Path path) throws IOException { + if (this.adventAccessService.checkAccess(path)) { + return this.thumbnailPathPlugin.getOutputStream(path); + } else { + throw new IOException("You don't yet have access to this resource."); + } + } + + private Optional getDefaultThumbnailPath(Path path) { + try { + return Files.list(getAbsolutePath(path).getParent().resolve(".thumbnails")) + .filter(f -> f.getFileName().toString().matches("^00(\\.[^.]+)*$")) + .findFirst(); + } catch (IOException ignored) { + return Optional.empty(); + } + } +} diff --git a/src/main/java/sh/rhiobet/lalafin/file/FileConfiguration.java b/src/main/java/sh/rhiobet/lalafin/file/FileConfiguration.java index ecae5df..d0d8455 100644 --- a/src/main/java/sh/rhiobet/lalafin/file/FileConfiguration.java +++ b/src/main/java/sh/rhiobet/lalafin/file/FileConfiguration.java @@ -4,7 +4,7 @@ import java.util.List; import java.util.Optional; import io.smallrye.config.ConfigMapping; -@ConfigMapping(prefix = "file") +@ConfigMapping(prefix = "lalafin.file") public interface FileConfiguration { public String directory(); public Optional> ignored(); diff --git a/src/main/java/sh/rhiobet/lalafin/file/FileMetadataService.java b/src/main/java/sh/rhiobet/lalafin/file/FileMetadataService.java new file mode 100644 index 0000000..50a3eb0 --- /dev/null +++ b/src/main/java/sh/rhiobet/lalafin/file/FileMetadataService.java @@ -0,0 +1,94 @@ +package sh.rhiobet.lalafin.file; + +import java.io.IOException; +import java.util.Optional; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import sh.rhiobet.lalafin.LalafinConfiguration; +import sh.rhiobet.lalafin.api.advent.AdventAccessService; +import sh.rhiobet.lalafin.api.internal.redis.FileTokenProvider; +import sh.rhiobet.lalafin.api.model.FileInfo; +import sh.rhiobet.lalafin.api.model.FileInfoBase; +import sh.rhiobet.lalafin.api.model.FolderInfo; +import sh.rhiobet.lalafin.path.Path; +import sh.rhiobet.lalafin.path.PathPlugin; + +@ApplicationScoped +public class FileMetadataService { + @Inject + LalafinConfiguration lalafinConfiguration; + + @Inject + FileConfiguration fileConfiguration; + + @Inject + ThumbnailService thumbnailService; + + @Inject + @Named("file/thumbnail") + PathPlugin thumbnailPathPlugin; + + @Inject + AdventAccessService adventAccessService; + + public FileInfoBase getInfo(Path filePath, FileTokenProvider fileTokenProvider) { + if (filePath.exists()) { + Optional thumbUrl = this.thumbnailPathPlugin.resolveURI(filePath); + + if (filePath.canHaveChildren()) { + FolderInfo folderInfo = new FolderInfo( + filePath.getFilename(), + thumbUrl.isPresent() ? "/file" + thumbUrl.get() : "", + "/file" + filePath.getURI(), + "" + ); + + try { + for (Path child : filePath.getChildren()) { + if (child.getFilename().startsWith(".")) { + continue; + } + if (fileConfiguration.ignored().isPresent()) { + for (String ignoreString : fileConfiguration.ignored().get()) { + if (child.getFilename().endsWith(ignoreString)) { + continue; + } + } + } + + Optional childThumbUrl = this.thumbnailPathPlugin.resolveURI(child); + + FileInfoBase contentInfo; + if (child.canHaveChildren()) { + contentInfo = new FolderInfo(child.getFilename(), "/file" + child.getURI()); + } else { + contentInfo = new FileInfo(child.getFilename(), "/file" + child.getURI()); + } + + if (childThumbUrl.isPresent()) { + contentInfo.thumbnailUrl = "/file" + childThumbUrl.get(); + } + + folderInfo.content.add(contentInfo); + } + } catch (IOException e) { + e.printStackTrace(); + } + + return folderInfo; + } else { + FileInfo fileInfo = new FileInfo( + filePath.getFilename(), + thumbUrl.isPresent() ? "/file" + thumbUrl.get() : "", + "/file" + filePath.getURI(), + "" + ); + + return fileInfo; + } + } + + return null; + } +} diff --git a/src/main/java/sh/rhiobet/lalafin/thumbnail/ThumbnailPathPlugin.java b/src/main/java/sh/rhiobet/lalafin/file/FileThumbnailPathPlugin.java similarity index 71% rename from src/main/java/sh/rhiobet/lalafin/thumbnail/ThumbnailPathPlugin.java rename to src/main/java/sh/rhiobet/lalafin/file/FileThumbnailPathPlugin.java index e3eb5b5..b5f3c1f 100644 --- a/src/main/java/sh/rhiobet/lalafin/thumbnail/ThumbnailPathPlugin.java +++ b/src/main/java/sh/rhiobet/lalafin/file/FileThumbnailPathPlugin.java @@ -1,4 +1,4 @@ -package sh.rhiobet.lalafin.thumbnail; +package sh.rhiobet.lalafin.file; import java.io.IOException; import java.io.OutputStream; @@ -10,7 +10,6 @@ import java.util.regex.Pattern; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import jakarta.inject.Named; -import sh.rhiobet.lalafin.api.configuration.FileApiConfiguration; import sh.rhiobet.lalafin.path.FileSystemPath; import sh.rhiobet.lalafin.path.Path; import sh.rhiobet.lalafin.path.PathAccessor; @@ -18,21 +17,22 @@ import sh.rhiobet.lalafin.path.PathPlugin; import sh.rhiobet.lalafin.path.ZipEntryPath; @ApplicationScoped -@Named("thumbnail") -public class ThumbnailPathPlugin extends PathAccessor implements PathPlugin { +@Named("file/thumbnail") +public class FileThumbnailPathPlugin extends PathAccessor implements PathPlugin { @Inject - FileApiConfiguration fileApiConfiguration; + FileConfiguration fileConfiguration; - @Override - public Optional resolveURI(Path path) { + @Override + public Optional resolveURI(Path path) { switch (path) { case FileSystemPath fsp: Optional thumbnailAbsolutePath = getThumbnailPath(path); if (thumbnailAbsolutePath.isPresent()) { - java.nio.file.Path rootFolderPath = Paths.get(this.fileApiConfiguration.directory()); - return Optional.of(rootFolderPath.resolve("file").relativize(thumbnailAbsolutePath.get()) - .toUri().getRawPath()); + java.nio.file.Path rootFolderPath = Paths.get(this.fileConfiguration.directory()); + return Optional.of( + "/" + rootFolderPath.resolve("file").relativize(thumbnailAbsolutePath.get()) + .toString()); } else { return Optional.empty(); } @@ -40,10 +40,10 @@ public class ThumbnailPathPlugin extends PathAccessor implements PathPlugin { case ZipEntryPath zep: return Optional.empty(); } - } + } - @Override - public OutputStream getOutputStream(Path path) throws IOException { + @Override + public OutputStream getOutputStream(Path path) throws IOException { switch (path) { case FileSystemPath fsp: Optional thumbnailAbsolutePath = getThumbnailPath(path); @@ -58,16 +58,16 @@ public class ThumbnailPathPlugin extends PathAccessor implements PathPlugin { case ZipEntryPath zep: throw new IOException("A zip file is not a valid location to store a thumbnail."); } - } + } private Optional getThumbnailPath(Path path) { - try { - return Files.list(getAbsolutePath(path).getParent().resolve(".thumbnails")) - .filter(f -> f.getFileName().toString() + try { + return Files.list(getAbsolutePath(path).getParent().resolve(".thumbnails")) + .filter(f -> f.getFileName().toString() .matches("^" + Pattern.quote(path.getFilename()) + "(\\.[^.]+)*$")) - .findFirst(); - } catch (IOException ignored) { + .findFirst(); + } catch (IOException ignored) { return Optional.empty(); - } + } } } diff --git a/src/main/java/sh/rhiobet/lalafin/file/rest/FilePrivateAPI.java b/src/main/java/sh/rhiobet/lalafin/file/rest/FilePrivateAPI.java new file mode 100644 index 0000000..940068c --- /dev/null +++ b/src/main/java/sh/rhiobet/lalafin/file/rest/FilePrivateAPI.java @@ -0,0 +1,63 @@ +package sh.rhiobet.lalafin.file.rest; + +import java.util.ArrayList; +import java.util.List; + +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.PathSegment; +import jakarta.ws.rs.core.Response; +import io.quarkus.security.Authenticated; +import sh.rhiobet.lalafin.access.AccessService; +import sh.rhiobet.lalafin.api.model.FileInfoBase; +import sh.rhiobet.lalafin.file.FileMetadataService; +import sh.rhiobet.lalafin.path.InvalidPathException; +import sh.rhiobet.lalafin.path.PathFactory; + +@Authenticated +@Path("/api/v1/private/file") +public class FilePrivateAPI { + @Inject + FileMetadataService fileMetadataService; + + @Inject + Instance accessServices; + + @Inject + PathFactory pathFactory; + + @GET + @Path("/") + @Produces(MediaType.APPLICATION_JSON) + public Response getFileInfo() { + return this.getFileInfo(new ArrayList<>()); + } + + @GET + @Path("/{names: .+}") + @Produces(MediaType.APPLICATION_JSON) + public Response getFileInfo(List names) { + sh.rhiobet.lalafin.path.Path path; + try { + path = this.pathFactory.toPath(names); + } catch (InvalidPathException ignored) { + return Response.status(Response.Status.BAD_REQUEST).build(); + } + + if (accessServices.stream().anyMatch((a -> !a.checkAccess(path)))) { + return Response.status(Response.Status.FORBIDDEN).build(); + } + + FileInfoBase fileInfo = this.fileMetadataService.getInfo(path, null); + + if (fileInfo == null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + + return Response.ok(fileInfo).build(); + } +} diff --git a/src/main/java/sh/rhiobet/lalafin/path/FileSystemPath.java b/src/main/java/sh/rhiobet/lalafin/path/FileSystemPath.java index d85a6da..99bd0de 100644 --- a/src/main/java/sh/rhiobet/lalafin/path/FileSystemPath.java +++ b/src/main/java/sh/rhiobet/lalafin/path/FileSystemPath.java @@ -31,6 +31,9 @@ public final class FileSystemPath implements Path { @Override public String getURI() { + if (this.segments.isEmpty()) { + return "/"; + } return this.segments.stream() .map(s -> "/" + Encode.encodePathSegment(s)) .reduce("", String::concat); @@ -38,16 +41,16 @@ public final class FileSystemPath implements Path { @Override public String getFilename() { - return this.segments.getLast(); + return this.segments.isEmpty() ? "/" : this.segments.getLast(); } @Override public long getSize() { try { - return Files.size(this.absolutePath); - } catch (IOException ignored) { + return Files.size(this.absolutePath); + } catch (IOException ignored) { return 0; - } + } } @Override @@ -62,7 +65,7 @@ public final class FileSystemPath implements Path { @Override public List getChildren() throws IOException { - if (this.segments.getLast().endsWith(".zip")) { + if (!this.segments.isEmpty() && this.segments.getLast().endsWith(".zip")) { try (ZipFile zipFile = new ZipFile(this.absolutePath.toFile())) { return zipFile.stream() .map(e -> (Path) new ZipEntryPath(this, e.getName()))