Прием и отдача файлов

This commit is contained in:
RoyceDa
2026-02-11 09:19:55 +02:00
parent dd10e59be3
commit e3523d224d
7 changed files with 210 additions and 2 deletions

View File

@@ -30,6 +30,11 @@
<artifactId>jersey-media-json-jackson</artifactId> <artifactId>jersey-media-json-jackson</artifactId>
<version>${jersey.version}</version> <version>${jersey.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-multipart</artifactId>
<version>${jersey.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.slf4j</groupId> <groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId> <artifactId>slf4j-simple</artifactId>

View File

@@ -1,7 +1,55 @@
package im.rosetta; package im.rosetta;
import im.rosetta.api.CdnResource;
import im.rosetta.storage.FileStore;
import org.glassfish.grizzly.http.server.HttpServer;
import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory;
import org.glassfish.jersey.media.multipart.MultiPartFeature;
import org.glassfish.jersey.server.ResourceConfig;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Path;
public class Main { public class Main {
public static void main(String[] args) { public static void main(String[] args) throws IOException, InterruptedException {
System.out.println("Hello world!"); int port = resolvePort(args);
Path filesDir = Path.of("files");
FileStore fileStore = new FileStore(filesDir);
ResourceConfig config = new ResourceConfig()
.register(MultiPartFeature.class)
.register(new CdnResource(fileStore));
URI baseUri = URI.create("http://0.0.0.0:" + port + "/");
HttpServer server = GrizzlyHttpServerFactory.createHttpServer(baseUri, config, false);
Runtime.getRuntime().addShutdownHook(new Thread(server::shutdownNow));
server.start();
System.out.println("CDN started at " + baseUri);
Thread.currentThread().join();
}
private static int resolvePort(String[] args) {
// Если порт указан аргументом — используем его.
if (args != null && args.length > 0) {
try {
return Integer.parseInt(args[0]);
} catch (NumberFormatException ignored) {
}
}
// Если порт задан в окружении — используем его.
String envPort = System.getenv("PORT");
if (envPort != null && !envPort.isBlank()) {
try {
return Integer.parseInt(envPort);
} catch (NumberFormatException ignored) {
}
}
// Значение по умолчанию.
return 8080;
} }
} }

View File

@@ -0,0 +1,85 @@
package im.rosetta.api;
import im.rosetta.storage.FileStore;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.StreamingOutput;
import org.glassfish.jersey.media.multipart.FormDataParam;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
@Path("/")
public class CdnResource {
private final FileStore fileStore;
public CdnResource(FileStore fileStore) {
this.fileStore = fileStore;
}
@POST
@Path("u")
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces(MediaType.APPLICATION_JSON)
public Response upload(@FormDataParam("file") InputStream inputStream) {
if (inputStream == null) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", "file is required"))
.build();
}
try {
String tag = fileStore.save(inputStream);
return Response.ok(Map.of("t", tag)).build();
} catch (IOException e) {
return Response.serverError()
.entity(Map.of("error", "upload failed"))
.build();
}
}
@GET
@Path("d/{tag}")
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response download(@PathParam("tag") String tag) {
if (!isValidTag(tag)) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", "invalid tag"))
.build();
}
try {
Optional<java.nio.file.Path> file = fileStore.getIfValid(tag);
if (file.isEmpty()) {
return Response.status(Response.Status.NOT_FOUND)
.entity(Map.of("error", "not found"))
.build();
}
StreamingOutput stream = output -> Files.copy(file.get(), output);
return Response.ok(stream)
.header("Content-Disposition", "attachment; filename=\"" + tag + "\"")
.build();
} catch (IOException e) {
return Response.serverError()
.entity(Map.of("error", "download failed"))
.build();
}
}
private boolean isValidTag(String tag) {
try {
UUID.fromString(tag);
return true;
} catch (IllegalArgumentException ex) {
return false;
}
}
}

View File

@@ -0,0 +1,70 @@
package im.rosetta.storage;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.time.Duration;
import java.time.Instant;
import java.util.Optional;
import java.util.UUID;
public class FileStore {
private static final Duration TTL = Duration.ofDays(7);
private final Path filesDir;
public FileStore(Path filesDir) throws IOException {
this.filesDir = filesDir.toAbsolutePath();
Files.createDirectories(this.filesDir);
cleanupExpired();
}
public String save(InputStream inputStream) throws IOException {
cleanupExpired();
String tag = UUID.randomUUID().toString();
Path target = filesDir.resolve(tag);
Files.copy(inputStream, target, StandardCopyOption.REPLACE_EXISTING);
Files.setLastModifiedTime(target, java.nio.file.attribute.FileTime.from(Instant.now()));
return tag;
}
public Optional<Path> getIfValid(String tag) throws IOException {
cleanupExpired();
Path target = filesDir.resolve(tag).normalize();
if (!target.startsWith(filesDir)) {
return Optional.empty();
}
if (!Files.exists(target)) {
return Optional.empty();
}
if (isExpired(target)) {
Files.deleteIfExists(target);
return Optional.empty();
}
return Optional.of(target);
}
/**
* Очищает неиспользуемые файлы, которые старше TTL. Вызывается при каждом сохранении и загрузке, а также при инициализации.
* @throws IOException
*/
public void cleanupExpired() throws IOException {
if (!Files.exists(filesDir)) {
return;
}
try (DirectoryStream<Path> stream = Files.newDirectoryStream(filesDir)) {
for (Path file : stream) {
if (Files.isRegularFile(file) && isExpired(file)) {
Files.deleteIfExists(file);
}
}
}
}
private boolean isExpired(Path file) throws IOException {
Instant lastModified = Files.getLastModifiedTime(file).toInstant();
return lastModified.plus(TTL).isBefore(Instant.now());
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.