Compare commits
8 Commits
master
...
c7ee90cbaa
| Author | SHA1 | Date | |
|---|---|---|---|
| c7ee90cbaa | |||
| 5e453dad51 | |||
| a4c0a4702a | |||
| 8c0bb2ce4a | |||
| 5924ca5647 | |||
| 3cc928ebc8 | |||
| 46b6f4867e | |||
| 1819861326 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -38,3 +38,5 @@ release.properties
|
|||||||
application.yaml
|
application.yaml
|
||||||
build.sh
|
build.sh
|
||||||
*log
|
*log
|
||||||
|
|
||||||
|
shell.nix
|
||||||
|
|||||||
3
pom.xml
3
pom.xml
@@ -8,8 +8,7 @@
|
|||||||
<properties>
|
<properties>
|
||||||
<compiler-plugin.version>3.12.1</compiler-plugin.version>
|
<compiler-plugin.version>3.12.1</compiler-plugin.version>
|
||||||
<maven.compiler.parameters>true</maven.compiler.parameters>
|
<maven.compiler.parameters>true</maven.compiler.parameters>
|
||||||
<maven.compiler.source>17</maven.compiler.source>
|
<maven.compiler.release>21</maven.compiler.release>
|
||||||
<maven.compiler.target>17</maven.compiler.target>
|
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||||
<quarkus-plugin.version>3.30.2</quarkus-plugin.version>
|
<quarkus-plugin.version>3.30.2</quarkus-plugin.version>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import java.util.List;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import io.smallrye.config.ConfigMapping;
|
import io.smallrye.config.ConfigMapping;
|
||||||
|
|
||||||
@ConfigMapping(prefix = "lalafin")
|
@ConfigMapping(prefix = "lalafin.core")
|
||||||
public interface LalafinConfiguration {
|
public interface LalafinConfiguration {
|
||||||
|
|
||||||
public String base_url();
|
public String base_url();
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package sh.rhiobet.lalafin.advent.access;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.inject.Named;
|
||||||
|
import sh.rhiobet.lalafin.advent.configuration.AdventConfiguration;
|
||||||
|
import sh.rhiobet.lalafin.core.access.AccessService;
|
||||||
|
import sh.rhiobet.lalafin.core.path.model.Path;
|
||||||
|
|
||||||
|
@ApplicationScoped
|
||||||
|
@Named("advent")
|
||||||
|
public class AdventAccessService implements AccessService {
|
||||||
|
@Inject
|
||||||
|
AdventConfiguration adventConfiguration;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean checkAccess(Path path) {
|
||||||
|
String pathUri = path.getURI();
|
||||||
|
|
||||||
|
Optional<AdventConfiguration.AdventEvent> matchingAdvent = adventConfiguration.events().stream()
|
||||||
|
.filter(e -> pathUri.startsWith(e.path()))
|
||||||
|
.findFirst();
|
||||||
|
|
||||||
|
if (matchingAdvent.isEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
String dayString = pathUri.replaceAll(
|
||||||
|
"^" + Pattern.quote(matchingAdvent.get().path()) + "/?([^.]+)\\.?[^.]*$",
|
||||||
|
"$1");
|
||||||
|
try {
|
||||||
|
int day = Integer.parseInt(dayString);
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
return ((now.getYear() > matchingAdvent.get().year())
|
||||||
|
|| (now.getYear() == matchingAdvent.get().year()
|
||||||
|
&& now.getMonthValue() > matchingAdvent.get().month())
|
||||||
|
|| (now.getYear() == matchingAdvent.get().year()
|
||||||
|
&& now.getMonthValue() == matchingAdvent.get().month()
|
||||||
|
&& now.getDayOfMonth() >= day));
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package sh.rhiobet.lalafin.advent.configuration;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import io.smallrye.config.ConfigMapping;
|
||||||
|
|
||||||
|
@ConfigMapping(prefix = "lalafin.advent")
|
||||||
|
public interface AdventConfiguration {
|
||||||
|
public List<AdventEvent> events();
|
||||||
|
|
||||||
|
public static interface AdventEvent {
|
||||||
|
public String path();
|
||||||
|
public int year();
|
||||||
|
public int month();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package sh.rhiobet.lalafin.advent.thumbnail;
|
||||||
|
|
||||||
|
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.advent.access.AdventAccessService;
|
||||||
|
import sh.rhiobet.lalafin.api.configuration.FileApiConfiguration;
|
||||||
|
import sh.rhiobet.lalafin.core.path.model.Path;
|
||||||
|
import sh.rhiobet.lalafin.core.path.model.PathAccessor;
|
||||||
|
import sh.rhiobet.lalafin.core.path.plugin.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<String> resolveURI(Path path) {
|
||||||
|
if (this.adventAccessService.checkAccess(path)) {
|
||||||
|
return this.thumbnailPathPlugin.resolveURI(path);
|
||||||
|
} else {
|
||||||
|
Optional<java.nio.file.Path> 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<java.nio.file.Path> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package sh.rhiobet.lalafin.core.access;
|
||||||
|
|
||||||
|
import sh.rhiobet.lalafin.core.path.model.Path;
|
||||||
|
|
||||||
|
public interface AccessService {
|
||||||
|
boolean checkAccess(Path path);
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package sh.rhiobet.lalafin.core.path.exception;
|
||||||
|
|
||||||
|
public class InvalidPathException extends Exception {
|
||||||
|
public InvalidPathException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package sh.rhiobet.lalafin.core.path.model;
|
||||||
|
|
||||||
|
import jakarta.ws.rs.core.PathSegment;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
import java.util.zip.ZipFile;
|
||||||
|
|
||||||
|
import org.jboss.resteasy.reactive.common.util.Encode;
|
||||||
|
|
||||||
|
public final class FileSystemPath implements Path {
|
||||||
|
private java.nio.file.Path rootFolderPath;
|
||||||
|
|
||||||
|
List<String> segments;
|
||||||
|
java.nio.file.Path absolutePath;
|
||||||
|
|
||||||
|
FileSystemPath(List<PathSegment> names, java.nio.file.Path rootFolderPath) {
|
||||||
|
this.segments = names.stream().map(n -> n.getPath()).toList();
|
||||||
|
this.rootFolderPath = rootFolderPath;
|
||||||
|
this.absolutePath = getAbsolutePath();
|
||||||
|
}
|
||||||
|
|
||||||
|
FileSystemPath(FileSystemPath parent, String fileName) {
|
||||||
|
this.segments = Stream.concat(parent.segments.stream(), Stream.of(fileName)).toList();
|
||||||
|
this.rootFolderPath = parent.rootFolderPath;
|
||||||
|
this.absolutePath = getAbsolutePath();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getURI() {
|
||||||
|
if (this.segments.isEmpty()) {
|
||||||
|
return "/";
|
||||||
|
}
|
||||||
|
return this.segments.stream()
|
||||||
|
.map(s -> "/" + Encode.encodePathSegment(s))
|
||||||
|
.reduce("", String::concat);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getFilename() {
|
||||||
|
return this.segments.isEmpty() ? "/" : this.segments.getLast();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getSize() {
|
||||||
|
try {
|
||||||
|
return Files.size(this.absolutePath);
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean exists() {
|
||||||
|
return Files.exists(this.absolutePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canHaveChildren() {
|
||||||
|
return Files.isDirectory(this.absolutePath) || this.segments.getLast().endsWith(".zip");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Path> getChildren() throws IOException {
|
||||||
|
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()))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
} else if (Files.isDirectory(this.absolutePath)){
|
||||||
|
return Files.list(this.absolutePath).sorted()
|
||||||
|
.map(f -> (Path) new FileSystemPath(this, f.getFileName().toString()))
|
||||||
|
.toList();
|
||||||
|
} else {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStream getInputStream() throws IOException {
|
||||||
|
return Files.newInputStream(this.absolutePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private java.nio.file.Path getAbsolutePath() {
|
||||||
|
return this.rootFolderPath.resolve("file").resolve(String.join("/", this.segments));
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/main/java/sh/rhiobet/lalafin/core/path/model/Path.java
Normal file
15
src/main/java/sh/rhiobet/lalafin/core/path/model/Path.java
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package sh.rhiobet.lalafin.core.path.model;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
sealed public interface Path permits FileSystemPath, ZipEntryPath {
|
||||||
|
String getURI();
|
||||||
|
String getFilename();
|
||||||
|
long getSize();
|
||||||
|
boolean exists();
|
||||||
|
boolean canHaveChildren();
|
||||||
|
List<Path> getChildren() throws IOException;
|
||||||
|
InputStream getInputStream() throws IOException;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package sh.rhiobet.lalafin.core.path.model;
|
||||||
|
|
||||||
|
public abstract class PathAccessor {
|
||||||
|
protected java.nio.file.Path getAbsolutePath(Path path) {
|
||||||
|
switch (path) {
|
||||||
|
case FileSystemPath fsp:
|
||||||
|
return fsp.absolutePath;
|
||||||
|
|
||||||
|
case ZipEntryPath zep:
|
||||||
|
return zep.zipFilePath.absolutePath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package sh.rhiobet.lalafin.core.path.model;
|
||||||
|
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.ws.rs.core.PathSegment;
|
||||||
|
|
||||||
|
import java.net.URLDecoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import sh.rhiobet.lalafin.api.configuration.FileApiConfiguration;
|
||||||
|
import sh.rhiobet.lalafin.core.path.exception.InvalidPathException;
|
||||||
|
|
||||||
|
@ApplicationScoped
|
||||||
|
public class PathFactory {
|
||||||
|
@Inject
|
||||||
|
FileApiConfiguration fileApiConfiguration;
|
||||||
|
|
||||||
|
public Path toPath(List<PathSegment> names) throws InvalidPathException {
|
||||||
|
if (hasEncodedPathSeparator(names)) {
|
||||||
|
throw new InvalidPathException("Some path segments contain illegal characters.");
|
||||||
|
} else {
|
||||||
|
java.nio.file.Path rootFolderPath = Paths.get(this.fileApiConfiguration.directory());
|
||||||
|
|
||||||
|
int zipIndex = -1;
|
||||||
|
for (int i = 0; i < names.size(); i++) {
|
||||||
|
if (names.get(i).getPath().endsWith(".zip")) {
|
||||||
|
zipIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (zipIndex >= 0 && zipIndex < names.size() - 1) {
|
||||||
|
FileSystemPath zipFilePath =
|
||||||
|
new FileSystemPath(names.subList(0, zipIndex + 1), rootFolderPath);
|
||||||
|
return new ZipEntryPath(zipFilePath, names.subList(zipIndex + 1, names.size()));
|
||||||
|
} else {
|
||||||
|
return new FileSystemPath(names, rootFolderPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasEncodedPathSeparator(List<PathSegment> names) {
|
||||||
|
return names.stream().anyMatch(s -> {
|
||||||
|
String current = s.getPath();
|
||||||
|
while (true) {
|
||||||
|
String decoded = URLDecoder.decode(current, StandardCharsets.UTF_8);
|
||||||
|
if (decoded.equals(current)) break;
|
||||||
|
current = decoded;
|
||||||
|
}
|
||||||
|
return current.contains("/") || current.contains("\0") || current.equals("..")
|
||||||
|
|| current.equals(".");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
package sh.rhiobet.lalafin.core.path.model;
|
||||||
|
|
||||||
|
import jakarta.ws.rs.core.PathSegment;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipFile;
|
||||||
|
|
||||||
|
import org.jboss.resteasy.reactive.common.util.Encode;
|
||||||
|
|
||||||
|
public final class ZipEntryPath implements Path {
|
||||||
|
private long size = 0;
|
||||||
|
private boolean exists = false;
|
||||||
|
|
||||||
|
FileSystemPath zipFilePath;
|
||||||
|
List<String> segments;
|
||||||
|
String entryName;
|
||||||
|
|
||||||
|
ZipEntryPath(FileSystemPath parent, List<PathSegment> entryName) {
|
||||||
|
this.zipFilePath = parent;
|
||||||
|
this.segments = entryName.stream().map(PathSegment::getPath).toList();
|
||||||
|
this.entryName = this.segments.stream().collect(Collectors.joining("/"));
|
||||||
|
}
|
||||||
|
|
||||||
|
ZipEntryPath(FileSystemPath parent, String entryName) {
|
||||||
|
this.zipFilePath = parent;
|
||||||
|
this.segments = Arrays.asList(entryName.split("/"));
|
||||||
|
this.entryName = entryName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getURI() {
|
||||||
|
return this.zipFilePath.getURI() + this.segments.stream()
|
||||||
|
.map(s -> "/" + Encode.encodePathSegment(s))
|
||||||
|
.reduce("", String::concat);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getFilename() {
|
||||||
|
return this.segments.getLast();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getSize() {
|
||||||
|
if (this.size == 0) {
|
||||||
|
fetchEntryData();
|
||||||
|
}
|
||||||
|
return this.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean exists() {
|
||||||
|
if (!this.exists) {
|
||||||
|
fetchEntryData();
|
||||||
|
}
|
||||||
|
return this.exists;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canHaveChildren() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Path> getChildren() throws IOException {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStream getInputStream() throws IOException {
|
||||||
|
if (this.zipFilePath.exists()) {
|
||||||
|
ZipFile zipFile = new ZipFile(this.zipFilePath.absolutePath.toFile());
|
||||||
|
ZipEntry zipEntry = zipFile.getEntry(this.entryName);
|
||||||
|
if (zipEntry != null) {
|
||||||
|
return new ZipEntryInputStream(zipFile, zipFile.getInputStream(zipEntry));
|
||||||
|
} else {
|
||||||
|
zipFile.close();
|
||||||
|
throw new IOException("Zip entry does not exist.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new IOException("Zip file does not exist.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fetchEntryData() {
|
||||||
|
if (!this.zipFilePath.exists()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try (ZipFile zipFile = new ZipFile(this.zipFilePath.absolutePath.toFile())) {
|
||||||
|
ZipEntry zipEntry = zipFile.getEntry(this.entryName);
|
||||||
|
this.exists = zipEntry != null;
|
||||||
|
|
||||||
|
if (this.exists) {
|
||||||
|
this.size = zipEntry.getSize();
|
||||||
|
}
|
||||||
|
} catch (IOException ignored) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ZipEntryInputStream extends InputStream {
|
||||||
|
private final ZipFile zipFile;
|
||||||
|
private final InputStream is;
|
||||||
|
|
||||||
|
public ZipEntryInputStream(ZipFile zipFile, InputStream is) {
|
||||||
|
this.zipFile = zipFile;
|
||||||
|
this.is = is;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read() throws IOException {
|
||||||
|
return this.is.read();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read(byte[] buf) throws IOException {
|
||||||
|
return this.is.read(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read(byte[] buf, int off, int len) throws IOException {
|
||||||
|
return this.is.read(buf, off, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] readAllBytes() throws IOException {
|
||||||
|
return this.is.readAllBytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] readNBytes(int len) throws IOException {
|
||||||
|
return this.is.readNBytes(len);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int readNBytes(byte[] buf, int off, int len) throws IOException {
|
||||||
|
return this.is.readNBytes(buf, off, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long skip(long n) throws IOException {
|
||||||
|
return this.is.skip(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int available() throws IOException {
|
||||||
|
return this.is.available();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
try {
|
||||||
|
this.is.close();
|
||||||
|
} finally {
|
||||||
|
this.zipFile.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package sh.rhiobet.lalafin.core.path.plugin;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import sh.rhiobet.lalafin.core.path.model.Path;
|
||||||
|
|
||||||
|
public interface PathPlugin {
|
||||||
|
Optional<String> resolveURI(Path path);
|
||||||
|
OutputStream getOutputStream(Path path) throws IOException;
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package sh.rhiobet.lalafin.file.access;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import io.quarkus.security.identity.SecurityIdentity;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.inject.Named;
|
||||||
|
import sh.rhiobet.lalafin.core.access.AccessService;
|
||||||
|
import sh.rhiobet.lalafin.core.path.model.Path;
|
||||||
|
import sh.rhiobet.lalafin.file.configuration.FileConfiguration;
|
||||||
|
|
||||||
|
@ApplicationScoped
|
||||||
|
@Named("file/role")
|
||||||
|
public class FileRoleAccessService implements AccessService {
|
||||||
|
@Inject
|
||||||
|
SecurityIdentity securityIdentity;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
FileConfiguration fileConfiguration;
|
||||||
|
|
||||||
|
public boolean checkAccess(Path path) {
|
||||||
|
String pathUri = path.getURI();
|
||||||
|
List<FileConfiguration.Route> matchingRoutes = fileConfiguration.routes().stream()
|
||||||
|
.filter(r -> pathUri.startsWith(r.path()))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
|
||||||
|
if (matchingRoutes.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return matchingRoutes.stream()
|
||||||
|
.allMatch(r -> r.roles().isEmpty()
|
||||||
|
|| this.securityIdentity.getRoles().containsAll(r.roles().get()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package sh.rhiobet.lalafin.file.configuration;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import io.smallrye.config.ConfigMapping;
|
||||||
|
|
||||||
|
@ConfigMapping(prefix = "lalafin.file")
|
||||||
|
public interface FileConfiguration {
|
||||||
|
public String directory();
|
||||||
|
public Optional<List<String>> ignored();
|
||||||
|
public List<Route> routes();
|
||||||
|
|
||||||
|
public static interface Route {
|
||||||
|
public String path();
|
||||||
|
public Optional<List<String>> roles();
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/main/java/sh/rhiobet/lalafin/file/model/FileInfo.java
Normal file
17
src/main/java/sh/rhiobet/lalafin/file/model/FileInfo.java
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package sh.rhiobet.lalafin.file.model;
|
||||||
|
|
||||||
|
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||||
|
|
||||||
|
@RegisterForReflection
|
||||||
|
public class FileInfo extends FileInfoBase {
|
||||||
|
public String publicApiUrl;
|
||||||
|
|
||||||
|
public FileInfo(String filename, String thumbnailUrl, String directUrl, String publicApiUrl) {
|
||||||
|
super(filename, "file", thumbnailUrl, directUrl);
|
||||||
|
this.publicApiUrl = publicApiUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public FileInfo(String filename, String directUrl) {
|
||||||
|
this(filename, "", directUrl, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package sh.rhiobet.lalafin.file.model;
|
||||||
|
|
||||||
|
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||||
|
|
||||||
|
@RegisterForReflection
|
||||||
|
public abstract class FileInfoBase implements Comparable<FileInfoBase> {
|
||||||
|
public String filename;
|
||||||
|
public String type;
|
||||||
|
public String thumbnailUrl;
|
||||||
|
public String directUrl;
|
||||||
|
public String viewUrl;
|
||||||
|
|
||||||
|
public FileInfoBase(String filename, String type, String thumbnailUrl, String directUrl,
|
||||||
|
String viewUrl) {
|
||||||
|
this.filename = filename;
|
||||||
|
this.type = type;
|
||||||
|
this.thumbnailUrl = thumbnailUrl;
|
||||||
|
this.directUrl = directUrl;
|
||||||
|
this.viewUrl = viewUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public FileInfoBase(String filename, String type, String thumbnailUrl, String directUrl) {
|
||||||
|
this(filename, type, thumbnailUrl, directUrl, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compareTo(FileInfoBase f) {
|
||||||
|
return this.filename.compareToIgnoreCase(f.filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package sh.rhiobet.lalafin.file.model;
|
||||||
|
|
||||||
|
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.internal.redis.FileTokenProvider;
|
||||||
|
import sh.rhiobet.lalafin.core.path.model.Path;
|
||||||
|
import sh.rhiobet.lalafin.core.path.plugin.PathPlugin;
|
||||||
|
import sh.rhiobet.lalafin.file.ThumbnailService;
|
||||||
|
import sh.rhiobet.lalafin.file.configuration.FileConfiguration;
|
||||||
|
|
||||||
|
@ApplicationScoped
|
||||||
|
public class FileMetadataService {
|
||||||
|
@Inject
|
||||||
|
LalafinConfiguration lalafinConfiguration;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
FileConfiguration fileConfiguration;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
ThumbnailService thumbnailService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@Named("file/thumbnail")
|
||||||
|
PathPlugin thumbnailPathPlugin;
|
||||||
|
|
||||||
|
public FileInfoBase getInfo(Path filePath, FileTokenProvider fileTokenProvider) {
|
||||||
|
if (filePath.exists()) {
|
||||||
|
Optional<String> 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<String> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/main/java/sh/rhiobet/lalafin/file/model/FolderInfo.java
Normal file
22
src/main/java/sh/rhiobet/lalafin/file/model/FolderInfo.java
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package sh.rhiobet.lalafin.file.model;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||||
|
|
||||||
|
@RegisterForReflection
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
public class FolderInfo extends FileInfoBase {
|
||||||
|
public Set<FileInfoBase> content;
|
||||||
|
public FileInfo playlist;
|
||||||
|
|
||||||
|
public FolderInfo(String filename, String thumbnailUrl, String directUrl, String viewUrl) {
|
||||||
|
super(filename, "folder", thumbnailUrl, directUrl, viewUrl);
|
||||||
|
this.content = new TreeSet<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public FolderInfo(String filename, String directUrl) {
|
||||||
|
this(filename, "", directUrl, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.core.access.AccessService;
|
||||||
|
import sh.rhiobet.lalafin.core.path.exception.InvalidPathException;
|
||||||
|
import sh.rhiobet.lalafin.core.path.model.PathFactory;
|
||||||
|
import sh.rhiobet.lalafin.file.model.FileInfoBase;
|
||||||
|
import sh.rhiobet.lalafin.file.model.FileMetadataService;
|
||||||
|
|
||||||
|
@Authenticated
|
||||||
|
@Path("/v1/api/private/file")
|
||||||
|
public class FilePrivateAPI {
|
||||||
|
@Inject
|
||||||
|
FileMetadataService fileMetadataService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
Instance<AccessService> 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<PathSegment> names) {
|
||||||
|
sh.rhiobet.lalafin.core.path.model.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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package sh.rhiobet.lalafin.file.thumbnail;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.inject.Named;
|
||||||
|
import sh.rhiobet.lalafin.file.configuration.FileConfiguration;
|
||||||
|
import sh.rhiobet.lalafin.core.path.model.FileSystemPath;
|
||||||
|
import sh.rhiobet.lalafin.core.path.model.Path;
|
||||||
|
import sh.rhiobet.lalafin.core.path.model.PathAccessor;
|
||||||
|
import sh.rhiobet.lalafin.core.path.model.ZipEntryPath;
|
||||||
|
import sh.rhiobet.lalafin.core.path.plugin.PathPlugin;
|
||||||
|
|
||||||
|
@ApplicationScoped
|
||||||
|
@Named("file/thumbnail")
|
||||||
|
public class FileThumbnailPathPlugin extends PathAccessor implements PathPlugin {
|
||||||
|
@Inject
|
||||||
|
FileConfiguration fileConfiguration;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<String> resolveURI(Path path) {
|
||||||
|
switch (path) {
|
||||||
|
case FileSystemPath fsp:
|
||||||
|
Optional<java.nio.file.Path> thumbnailAbsolutePath = getThumbnailPath(path);
|
||||||
|
|
||||||
|
if (thumbnailAbsolutePath.isPresent()) {
|
||||||
|
java.nio.file.Path rootFolderPath = Paths.get(this.fileConfiguration.directory());
|
||||||
|
return Optional.of(
|
||||||
|
"/" + rootFolderPath.resolve("file").relativize(thumbnailAbsolutePath.get())
|
||||||
|
.toString());
|
||||||
|
} else {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
case ZipEntryPath zep:
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OutputStream getOutputStream(Path path) throws IOException {
|
||||||
|
switch (path) {
|
||||||
|
case FileSystemPath fsp:
|
||||||
|
Optional<java.nio.file.Path> thumbnailAbsolutePath = getThumbnailPath(path);
|
||||||
|
if (thumbnailAbsolutePath.isPresent()) {
|
||||||
|
Files.delete(thumbnailAbsolutePath.get());
|
||||||
|
}
|
||||||
|
java.nio.file.Path newThumbnailAbsolutePath =
|
||||||
|
getAbsolutePath(path).getParent().resolve(".thumbnails").resolve(path.getFilename());
|
||||||
|
Files.createDirectories(newThumbnailAbsolutePath.getParent());
|
||||||
|
return Files.newOutputStream(newThumbnailAbsolutePath);
|
||||||
|
|
||||||
|
case ZipEntryPath zep:
|
||||||
|
throw new IOException("A zip file is not a valid location to store a thumbnail.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<java.nio.file.Path> getThumbnailPath(Path path) {
|
||||||
|
try {
|
||||||
|
return Files.list(getAbsolutePath(path).getParent().resolve(".thumbnails"))
|
||||||
|
.filter(f -> f.getFileName().toString()
|
||||||
|
.matches("^" + Pattern.quote(path.getFilename()) + "(\\.[^.]+)*$"))
|
||||||
|
.findFirst();
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user