Прием и отдача файлов
This commit is contained in:
5
pom.xml
5
pom.xml
@@ -30,6 +30,11 @@
|
||||
<artifactId>jersey-media-json-jackson</artifactId>
|
||||
<version>${jersey.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.glassfish.jersey.media</groupId>
|
||||
<artifactId>jersey-media-multipart</artifactId>
|
||||
<version>${jersey.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-simple</artifactId>
|
||||
|
||||
@@ -1,7 +1,55 @@
|
||||
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 static void main(String[] args) {
|
||||
System.out.println("Hello world!");
|
||||
public static void main(String[] args) throws IOException, InterruptedException {
|
||||
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;
|
||||
}
|
||||
}
|
||||
85
src/main/java/im/rosetta/api/CdnResource.java
Normal file
85
src/main/java/im/rosetta/api/CdnResource.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
70
src/main/java/im/rosetta/storage/FileStore.java
Normal file
70
src/main/java/im/rosetta/storage/FileStore.java
Normal 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.
BIN
target/classes/im/rosetta/api/CdnResource.class
Normal file
BIN
target/classes/im/rosetta/api/CdnResource.class
Normal file
Binary file not shown.
BIN
target/classes/im/rosetta/storage/FileStore.class
Normal file
BIN
target/classes/im/rosetta/storage/FileStore.class
Normal file
Binary file not shown.
Reference in New Issue
Block a user