refactory

This commit is contained in:
2022-04-29 12:35:14 +03:00
parent a868b66e0c
commit cbfe2c50a7
32 changed files with 603 additions and 383 deletions

View File

@@ -7,9 +7,7 @@ buildscript {
} }
dependencies { dependencies {
// https://plugins.gradle.org/plugin/org.springframework.boot // https://plugins.gradle.org/plugin/org.springframework.boot
classpath "org.springframework.boot:spring-boot-gradle-plugin:${pluginSpringBootVerson}" classpath "org.springframework.boot:spring-boot-gradle-plugin:${springBootVerson}"
// https://plugins.gradle.org/plugin/io.spring.dependency-management
classpath "io.spring.gradle:dependency-management-plugin:${pluginSpringBomVerson}"
} }
} }
@@ -29,9 +27,15 @@ subprojects {
dependencies { dependencies {
annotationProcessor("org.projectlombok:lombok:${lombokVersion}") annotationProcessor("org.projectlombok:lombok:${lombokVersion}")
compileOnly("org.projectlombok:lombok:${lombokVersion}") compileOnly("org.projectlombok:lombok:${lombokVersion}")
testAnnotationProcessor("org.projectlombok:lombok:${lombokVersion}")
testCompileOnly("org.projectlombok:lombok:${lombokVersion}")
testImplementation("org.junit.jupiter:junit-jupiter-api:${junitJupiterVersion}")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${junitJupiterVersion}")
testImplementation("org.mockito:mockito-core:${mockitoCoreVersion}")
} }
configurations.implementation.resolutionStrategy { test {
failOnVersionConflict() useJUnitPlatform()
} }
} }

View File

@@ -1,5 +0,0 @@
apply plugin: 'java-library'
dependencies {
api("com.squareup.okhttp3:okhttp:${okhttpVersion}")
}

View File

@@ -1,6 +0,0 @@
package ru.di9.mirror.app.domain;
import java.io.InputStream;
public record FileRecord(String name, long size, InputStream inputStream) {
}

View File

@@ -1,4 +0,0 @@
package ru.di9.mirror.app.domain;
public record PathRecord(String name, PathType type) {
}

View File

@@ -1,7 +0,0 @@
package ru.di9.mirror.app.domain;
import java.util.List;
public record WalkerResult(PathType pathType, FileRecord fileRecord, List<PathRecord> filesList) {
}

View File

@@ -1,112 +0,0 @@
package ru.di9.mirror.app.service;
import ru.di9.mirror.app.Utils;
import ru.di9.mirror.app.domain.FileRecord;
import ru.di9.mirror.app.domain.PathRecord;
import ru.di9.mirror.app.domain.PathType;
import ru.di9.mirror.app.domain.WalkerResult;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
public class FileStorageService implements StorageService {
private final Path storagePath;
public FileStorageService(String storagePath) {
this.storagePath = Paths.get(storagePath).resolve("local");
}
@SuppressWarnings("java:S112")
@Override
public Optional<FileRecord> getFile(String path) {
Path fullPath = storageResolve(path);
if (Files.notExists(fullPath)) {
return Optional.empty();
}
try {
return Optional.of(new FileRecord(
fullPath.toFile().getName(),
Files.size(fullPath),
Files.newInputStream(fullPath)
));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings("java:S112")
@Override
public void putFile(String path, InputStream inputStream) {
Path fullPath = storageResolve(path);
try {
Files.createDirectories(fullPath.getParent());
} catch (IOException e) {
throw new RuntimeException(e);
}
try (OutputStream outputStream = Files.newOutputStream(fullPath)) {
byte[] buff = new byte[8 * 1024];
int len;
while ((len = inputStream.read(buff)) > 0) {
outputStream.write(buff, 0, len);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings("java:S112")
@Override
public Optional<WalkerResult> walker(String path) {
Path fullPath = storageResolve(path);
if (Files.notExists(fullPath)) {
return Optional.empty();
}
if (Files.isDirectory(fullPath)) {
final List<PathRecord> list = new ArrayList<>();
try (Stream<Path> walk = Files.walk(fullPath, 1)) {
walk.skip(1).forEach(path1 -> list.add(new PathRecord(
path1.getFileName().toString(),
Files.isDirectory(path1) ? PathType.DIRECTORY : PathType.FILE)));
} catch (IOException e) {
throw new RuntimeException(e);
}
return Optional.of(new WalkerResult(PathType.DIRECTORY, null, list));
} else {
try {
var fileRecord = new FileRecord(
fullPath.getFileName().toString(),
Files.size(fullPath),
Files.newInputStream(fullPath)
);
return Optional.of(new WalkerResult(PathType.FILE, fileRecord, null));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
private Path storageResolve(String httpPath) {
if (Utils.isEmptyString(httpPath)) {
return storagePath;
} else {
return storagePath.resolve(httpPath.substring(1));
}
}
}

View File

@@ -1,44 +0,0 @@
package ru.di9.mirror.app.service;
import lombok.SneakyThrows;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import ru.di9.mirror.app.domain.FileRecord;
import java.io.IOException;
import java.util.Objects;
import java.util.Optional;
public class MavenCentralStorageService extends MirrorService {
private static final String MAVEN_CENTRAL_URL = "https://repo1.maven.org/maven2";
private final OkHttpClient okHttpClient;
public MavenCentralStorageService(OkHttpClient okHttpClient) {
this.okHttpClient = okHttpClient;
}
@Override
@SneakyThrows
public Optional<FileRecord> getFile(String path) {
Request request = new Request.Builder()
.url(MAVEN_CENTRAL_URL + path)
.build();
Response response = okHttpClient.newCall(request).execute();
if (!response.isSuccessful()) {
if (response.code() == 404) {
return Optional.empty();
}
throw new IOException("Unexpected code " + response);
}
return Optional.of(new FileRecord(
path,
Long.parseLong(Objects.requireNonNull(response.header("Content-Length"))),
Objects.requireNonNull(response.body()).byteStream()
));
}
}

View File

@@ -1,19 +0,0 @@
package ru.di9.mirror.app.service;
import ru.di9.mirror.app.domain.WalkerResult;
import java.io.InputStream;
import java.util.Optional;
public abstract class MirrorService implements StorageService{
@Override
public void putFile(String path, InputStream inputStream) {
throw new UnsupportedOperationException();
}
@Override
public Optional<WalkerResult> walker(String path) {
throw new UnsupportedOperationException();
}
}

View File

@@ -1,52 +0,0 @@
package ru.di9.mirror.app.service;
import ru.di9.mirror.app.domain.FileRecord;
import ru.di9.mirror.app.domain.WalkerResult;
import java.io.InputStream;
import java.util.List;
import java.util.Optional;
@SuppressWarnings("ClassCanBeRecord")
public class MirroredStorageService implements StorageService {
private final FileStorageService fileStorageService;
private final List<MirrorService> mirrors;
public MirroredStorageService(FileStorageService fileStorageService,
List<MirrorService> mirrors) {
this.fileStorageService = fileStorageService;
this.mirrors = mirrors;
}
@Override
public Optional<FileRecord> getFile(String path) {
Optional<FileRecord> optional;
optional = fileStorageService.getFile(path);
if (optional.isPresent()) {
return optional;
}
for (StorageService mirror : mirrors) {
optional = mirror.getFile(path);
if (optional.isPresent()) {
FileRecord fileRecord = optional.get();
fileStorageService.putFile(fileRecord.name(), fileRecord.inputStream());
break;
}
}
return fileStorageService.getFile(path);
}
@Override
public void putFile(String path, InputStream inputStream) {
fileStorageService.putFile(path, inputStream);
}
@Override
public Optional<WalkerResult> walker(String path) {
return fileStorageService.walker(path);
}
}

View File

@@ -1,16 +0,0 @@
package ru.di9.mirror.app.service;
import ru.di9.mirror.app.domain.FileRecord;
import ru.di9.mirror.app.domain.WalkerResult;
import java.io.InputStream;
import java.util.Optional;
public interface StorageService {
Optional<FileRecord> getFile(String path);
void putFile(String path, InputStream inputStream);
Optional<WalkerResult> walker(String path);
}

33
mirror-core/build.gradle Normal file
View File

@@ -0,0 +1,33 @@
apply plugin: 'java-library'
apply plugin: 'idea'
configurations {
integrationTestImplementation {
extendsFrom(testImplementation)
}
integrationTestRuntimeOnly {
extendsFrom(testRuntimeOnly)
}
}
sourceSets {
integrationTest {
java {
compileClasspath += main.output + test.output
runtimeClasspath += main.output + test.output
srcDir file('src/integrationTest/java')
}
resources {
srcDir file('src/integrationTest/resources')
}
}
}
idea.module {
testSourceDirs += sourceSets.integrationTest.java.srcDirs
testResourceDirs += sourceSets.integrationTest.resources.srcDirs
}
dependencies {
api("io.minio:minio:${minioVersion}")
}

View File

@@ -0,0 +1,68 @@
package ru.di9.mirror.core.service;
import io.minio.MinioClient;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import ru.di9.mirror.core.domain.ItemRecord2;
import java.io.InputStream;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
class MinioServiceIntTest {
static final String url = "http://dev.di9.ru:9000";
static final String accessKey = "mirror";
static final String secretKey = "mirror123";
static final String bucket = "mirror";
static MinioClient minioClient;
MinioService minioService;
@BeforeAll
static void beforeAll() {
minioClient = MinioClient.builder()
.endpoint(url)
.credentials(accessKey, secretKey)
.build();
}
@BeforeEach
void setUp() {
minioService = new MinioService(minioClient, bucket);
}
@Test
void list() {
List<ItemRecord2> list = minioService.list("/local/ghast/ghast-tools/");
assertNotNull(list);
assertFalse(list.isEmpty());
assertTrue(list.stream()
.anyMatch(itemRecord2 -> itemRecord2.name().equals("maven-metadata.xml")
&& !itemRecord2.isDir()
&& itemRecord2.size() == 320));
assertTrue(list.stream()
.anyMatch(itemRecord2 -> itemRecord2.name().equals("1.13")
&& itemRecord2.isDir()
&& itemRecord2.size() == 0));
}
@Test
void notExists() {
Optional<InputStream> optional = minioService.get("/not/exists/path/to/object");
assertNotNull(optional);
assertTrue(optional.isEmpty());
}
@Test
void exists() {
Optional<InputStream> optional = minioService.get("/local/ghast/ghast-tools/maven-metadata.xml");
assertNotNull(optional);
assertTrue(optional.isPresent());
}
}

View File

@@ -1,4 +1,4 @@
package ru.di9.mirror.app; package ru.di9.mirror.core;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
@@ -9,4 +9,8 @@ public final class Utils {
public static boolean isEmptyString(String string) { public static boolean isEmptyString(String string) {
return string == null || string.isEmpty() || string.isBlank(); return string == null || string.isEmpty() || string.isBlank();
} }
public static boolean isNotEmptyString(String string) {
return !isEmptyString(string);
}
} }

View File

@@ -0,0 +1,28 @@
package ru.di9.mirror.core.domain;
import io.minio.Result;
import io.minio.messages.Item;
import lombok.SneakyThrows;
public record ItemRecord2(Result<Item> itemResult, String prefix) {
@SneakyThrows
public boolean isDir() {
return itemResult.get().isDir();
}
@SneakyThrows
public String name() {
String objectName = itemResult.get().objectName();
if (isDir()) {
return objectName.substring(prefix.length() - 1, objectName.length() - 1);
} else {
return objectName.substring(prefix.length() - 1);
}
}
@SneakyThrows
public long size() {
return itemResult.get().size();
}
}

View File

@@ -0,0 +1,6 @@
package ru.di9.mirror.core.domain;
import java.io.InputStream;
public record MavenHandlerGetFileResponse(String name, InputStream inputStream) {
}

View File

@@ -1,4 +1,4 @@
package ru.di9.mirror.app.domain; package ru.di9.mirror.core.domain;
public enum PathType { public enum PathType {
FILE, DIRECTORY FILE, DIRECTORY

View File

@@ -0,0 +1,5 @@
package ru.di9.mirror.core.domain;
public record WalkerResult(PathType type) {
}

View File

@@ -0,0 +1,18 @@
package ru.di9.mirror.core.handler;
import lombok.RequiredArgsConstructor;
import ru.di9.mirror.core.domain.ItemRecord2;
import ru.di9.mirror.core.service.MinioService;
import java.util.List;
@RequiredArgsConstructor
public class IndexOfHandler {
private final MinioService minioService;
public List<ItemRecord2> walker(String path) {
return minioService.list("/local/" + path);
}
}

View File

@@ -0,0 +1,50 @@
package ru.di9.mirror.core.handler;
import lombok.RequiredArgsConstructor;
import ru.di9.mirror.core.domain.MavenHandlerGetFileResponse;
import ru.di9.mirror.core.service.ExternalMavenService;
import ru.di9.mirror.core.service.MinioService;
import java.io.InputStream;
import java.util.List;
import java.util.Optional;
@RequiredArgsConstructor
public class MavenHandler {
private final MinioService minioService;
private final List<ExternalMavenService> externalMavenServices;
public Optional<MavenHandlerGetFileResponse> getFile(String path) {
final String fileName = path.substring(path.lastIndexOf("/") + 1);
Optional<InputStream> optionalInputStream = minioService.get("/local/" + path);
if (optionalInputStream.isPresent()) {
return optionalInputStream
.map(inputStream -> new MavenHandlerGetFileResponse(fileName, inputStream));
} else {
for (ExternalMavenService externalMavenService : externalMavenServices) {
final String nameForStore = "/" + externalMavenService.getId() + "/" + path;
optionalInputStream = minioService.get(nameForStore);
if (optionalInputStream.isPresent()) {
return optionalInputStream
.map(inputStream -> new MavenHandlerGetFileResponse(fileName, inputStream));
} else {
optionalInputStream = externalMavenService.getFile(path);
if (optionalInputStream.isPresent()) {
minioService.put(nameForStore, optionalInputStream.get());
return minioService.get(nameForStore)
.map(inputStream -> new MavenHandlerGetFileResponse(fileName, inputStream));
}
}
}
return Optional.empty();
}
}
public void putFile(String path, InputStream inputStream) {
minioService.put("/local/" + path, inputStream);
}
}

View File

@@ -0,0 +1,55 @@
package ru.di9.mirror.core.service;
import lombok.Getter;
import lombok.SneakyThrows;
import lombok.ToString;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import java.io.IOException;
import java.io.InputStream;
import java.util.Optional;
@SuppressWarnings("ClassCanBeRecord")
@ToString(of = {"id", "url"})
public class ExternalMavenService {
@Getter
private final String id;
private final String url;
private final OkHttpClient okHttpClient;
public ExternalMavenService(String id, String url, OkHttpClient okHttpClient) {
this.id = id;
this.okHttpClient = okHttpClient;
if (!url.endsWith("/")) {
this.url = url + "/";
} else {
this.url = url;
}
}
@SneakyThrows
public Optional<InputStream> getFile(String path) {
if (path.startsWith("/")) {
path = path.substring(1);
}
Request request = new Request.Builder()
.url(url + path)
.build();
Response response = okHttpClient.newCall(request).execute();
if (!response.isSuccessful()) {
if (response.code() == 404) {
return Optional.empty();
}
throw new IOException("Unexpected code " + response);
}
return Optional.ofNullable(response.body()).map(ResponseBody::byteStream);
}
}

View File

@@ -0,0 +1,68 @@
package ru.di9.mirror.core.service;
import io.minio.*;
import io.minio.errors.*;
import io.minio.messages.Item;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import ru.di9.mirror.core.domain.ItemRecord2;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@RequiredArgsConstructor
public class MinioService {
private static final long UNKNOWN_SIZE = -1L;
private static final long PART_SIZE = (5 * 1024 * 1024); // 5 MB
private final MinioClient minioClient;
private final String bucket;
@SneakyThrows
public void put(String name, InputStream inputStream) {
minioClient.putObject(PutObjectArgs.builder()
.bucket(bucket)
.object(name)
.stream(inputStream, UNKNOWN_SIZE, PART_SIZE)
.build());
}
@SneakyThrows
public Optional<InputStream> get(String name) {
try {
return Optional.of(minioClient.getObject(GetObjectArgs.builder()
.bucket(bucket)
.object(name)
.build()));
} catch (ErrorResponseException e) {
if (e.errorResponse().code().equalsIgnoreCase("NoSuchKey")) {
return Optional.empty();
} else {
throw e;
}
}
}
public List<ItemRecord2> list(String prefix) {
if (!prefix.endsWith("/")) {
prefix = prefix + "/";
}
Iterable<Result<Item>> results = minioClient.listObjects(ListObjectsArgs.builder()
.bucket(bucket)
.prefix(prefix)
.build());
List<ItemRecord2> list = new ArrayList<>();
for (Result<Item> result : results) {
list.add(new ItemRecord2(result, prefix));
}
return list;
}
}

View File

@@ -0,0 +1,63 @@
package ru.di9.mirror.core.service;
import io.minio.ListObjectsArgs;
import io.minio.MinioClient;
import io.minio.Result;
import io.minio.messages.Item;
import lombok.SneakyThrows;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import ru.di9.mirror.core.domain.ItemRecord2;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
class MinioServiceTest {
static final String bucket = "mirror";
static MinioClient mockMinioClient;
@BeforeAll
static void beforeAll() {
mockMinioClient = mock(MinioClient.class);
Iterable<Result<Item>> listObjects = List.of(
mockResult("ghast/ghast-tools/maven-metadata.xml", 320, false),
mockResult("ghast/ghast-tools/1.13/", 0, true)
);
when(mockMinioClient.listObjects(any(ListObjectsArgs.class))).thenReturn(listObjects);
}
@Test
void list() {
MinioService minioService = new MinioService(mockMinioClient, bucket);
List<ItemRecord2> list = minioService.list("/ghast/ghast-tools/");
assertNotNull(list);
assertFalse(list.isEmpty());
assertTrue(list.stream()
.anyMatch(itemRecord2 -> itemRecord2.name().equals("maven-metadata.xml")
&& !itemRecord2.isDir()
&& itemRecord2.size() == 320));
assertTrue(list.stream()
.anyMatch(itemRecord2 -> itemRecord2.name().equals("1.13")
&& itemRecord2.isDir()
&& itemRecord2.size() == 0));
}
@SuppressWarnings("unchecked")
@SneakyThrows
private static Result<Item> mockResult(String name, long size, boolean isDir) {
Item item = mock(Item.class);
when(item.objectName()).thenReturn(name);
when(item.size()).thenReturn(size);
when(item.isDir()).thenReturn(isDir);
Result<Item> result = mock(Result.class);
when(result.get()).thenReturn(item);
return result;
}
}

View File

@@ -1,11 +1,12 @@
//file:noinspection GrUnresolvedAccess
apply plugin: 'org.springframework.boot' apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
dependencies { dependencies {
implementation(project(':mirror-app')) implementation(project(':mirror-core'))
annotationProcessor('org.springframework.boot:spring-boot-configuration-processor') annotationProcessor("org.springframework.boot:spring-boot-configuration-processor:${springBootVerson}")
implementation('org.springframework.boot:spring-boot-starter-web') implementation("org.springframework.boot:spring-boot-starter-web:${springBootVerson}")
implementation("org.springframework.boot:spring-boot-starter-freemarker:${springBootVerson}")
} }
tasks.named('compileJava') { tasks.named('compileJava') {

View File

@@ -0,0 +1,20 @@
package ru.di9.mirror.web.config;
import lombok.Getter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
@Component
@ConfigurationProperties(prefix = "maven-mirrors")
public class MavenMirrorsProperties {
@Getter
private final List<MirrorData> list = new ArrayList<>();
public record MirrorData(String id, String url) {
}
}

View File

@@ -1,10 +1,14 @@
package ru.di9.mirror.web.config; package ru.di9.mirror.web.config;
import io.minio.MinioClient;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import ru.di9.mirror.app.service.*; import ru.di9.mirror.core.handler.IndexOfHandler;
import ru.di9.mirror.core.handler.MavenHandler;
import ru.di9.mirror.core.service.ExternalMavenService;
import ru.di9.mirror.core.service.MinioService;
import java.util.List; import java.util.List;
@@ -12,20 +16,42 @@ import java.util.List;
public class WebConfig { public class WebConfig {
@Bean @Bean
public StorageService mirroredStorageService(FileStorageService fileStorageService, public MavenHandler mavenHandler(MinioService minioService,
List<MirrorService> mirrors) { List<ExternalMavenService> externalMavenServices) {
return new MirroredStorageService(fileStorageService, mirrors); return new MavenHandler(minioService, externalMavenServices);
} }
@Bean @Bean
public FileStorageService fileStorageService(@Value("${maven.storage}") String storagePath) { public IndexOfHandler indexOfHandler(MinioService minioService) {
return new FileStorageService(storagePath); return new IndexOfHandler(minioService);
} }
@Bean @Bean
public List<MirrorService> mirrors() { public MinioService minioService(MinioClient minioClient, @Value("${minio.bucket}") String bucket) {
return List.of( return new MinioService(minioClient, bucket);
new MavenCentralStorageService(new OkHttpClient())
);
} }
@Bean
public MinioClient minioClient(@Value("${minio.url}") String url,
@Value("${minio.accessKey}") String accessKey,
@Value("${minio.secretKey}") String secretKey) {
return MinioClient.builder()
.endpoint(url)
.credentials(accessKey, secretKey)
.build();
}
@Bean
public List<ExternalMavenService> externalMavenServices(OkHttpClient okHttpClient,
MavenMirrorsProperties mavenMirrorsProperties) {
return mavenMirrorsProperties.getList().stream()
.map(mirrorData -> new ExternalMavenService(mirrorData.id(), mirrorData.url(), okHttpClient))
.toList();
}
@Bean
public OkHttpClient okHttpClient() {
return new OkHttpClient();
}
} }

View File

@@ -1,86 +1,79 @@
package ru.di9.mirror.web.controller; package ru.di9.mirror.web.controller;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import ru.di9.mirror.app.Utils; import ru.di9.mirror.core.Utils;
import ru.di9.mirror.app.domain.FileRecord; import ru.di9.mirror.core.domain.ItemRecord2;
import ru.di9.mirror.app.domain.PathRecord; import ru.di9.mirror.core.handler.IndexOfHandler;
import ru.di9.mirror.app.domain.PathType;
import ru.di9.mirror.app.domain.WalkerResult;
import ru.di9.mirror.app.service.StorageService;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional;
@Controller @Controller
@RequestMapping(path = "/maven") @RequestMapping(path = "/maven")
public class IndexOfMavenController { public class IndexOfMavenController {
private final StorageService storageService;
public IndexOfMavenController(@Qualifier("mirroredStorageService") StorageService storageService) { private final IndexOfHandler indexOfHandler;
this.storageService = storageService;
public IndexOfMavenController(IndexOfHandler indexOfHandler) {
this.indexOfHandler = indexOfHandler;
} }
@GetMapping(path = "/{*path}", headers = "accept=text/html") @GetMapping(path = "/{*path}", headers = "accept=text/html")
public ResponseEntity<Object> walker(@PathVariable("path") String httpPath) { public String walker(@PathVariable("path") String httpPath,
ResponseEntity<Object> responseEntity; @ModelAttribute("model") ModelMap model) {
String path = correctingHttpPath(httpPath);
Optional<WalkerResult> walkerResultOptional = storageService.walker(httpPath); List<ItemRecord2> walker = indexOfHandler.walker(path);
if (walkerResultOptional.isEmpty()) { List<ModelLink> links = new ArrayList<>();
responseEntity = ResponseEntity.status(HttpStatus.NOT_FOUND)
.body("<html><body><h1>404 - Not Found</h1><hr><p style=\"text-align:right\">Project-Mirror</p></body></html>"); StringBuilder sb = new StringBuilder("/maven");
} else { int resetLength = sb.length();
WalkerResult walkerResult = walkerResultOptional.get();
if (walkerResult.pathType() == PathType.DIRECTORY) { if (Utils.isNotEmptyString(path)) {
responseEntity = generateIndexOf(httpPath, walkerResult.filesList()); int idx = path.lastIndexOf("/");
} else { if (idx > -1) {
responseEntity = downloadFile(walkerResult.fileRecord()); sb.append("/").append(path, 0, idx);
} }
links.add(new ModelLink("..", sb.toString()));
} }
return responseEntity; sb.setLength(resetLength);
} sb.append("/");
resetLength = sb.length();
for (ItemRecord2 itemRecord2 : walker) {
if (Utils.isNotEmptyString(path)) {
sb.append(path).append("/");
}
sb.append(itemRecord2.name());
private ResponseEntity<Object> downloadFile(FileRecord fileRecord) { links.add(new ModelLink(itemRecord2.name() + (itemRecord2.isDir() ? "/" : ""), sb.toString()));
return ResponseEntity.ok() sb.setLength(resetLength);
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + fileRecord.name() + "\"")
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.contentLength(fileRecord.size())
.body(new InputStreamResource(fileRecord.inputStream()));
}
private ResponseEntity<Object> generateIndexOf(String httpPath, List<PathRecord> filesList) {
ResponseEntity<Object> responseEntity;
StringBuilder sb = new StringBuilder("<html><body><h1>Index of</h1><hr><ul>");
if (!Utils.isEmptyString(httpPath)) {
String levelUpPath = httpPath.substring(0, httpPath.lastIndexOf("/"));
sb.append("<li><a href=\"/maven").append(levelUpPath).append("\">..</a></li>");
} }
filesList.forEach(pathRecord -> { model.put("links", links);
sb.append("<li><a href=\"/maven") return "list";
.append(httpPath) }
.append("/")
.append(pathRecord.name())
.append("\">")
.append(pathRecord.name());
if (pathRecord.type() == PathType.DIRECTORY) {
sb.append("/");
}
sb.append("</a></li>");
});
responseEntity = ResponseEntity.ok(sb.append("</ul><hr><p style=\"text-align:right\">Project-Mirror</p></body></html>").toString()); private String correctingHttpPath(String httpPath) {
return responseEntity; String path = httpPath;
if (path.startsWith("/")) {
path = path.substring(1);
}
if (path.endsWith("/")) {
path = path.substring(0, path.length() - 1);
}
return path;
}
public record ModelLink(String name, String link) {
} }
} }

View File

@@ -1,7 +1,6 @@
package ru.di9.mirror.web.controller; package ru.di9.mirror.web.controller;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.io.InputStreamResource; import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
@@ -12,43 +11,31 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import ru.di9.mirror.app.domain.FileRecord; import ru.di9.mirror.core.handler.MavenHandler;
import ru.di9.mirror.app.service.StorageService;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.Optional;
@Slf4j @Slf4j
@Controller @Controller
@RequestMapping(path = "/maven") @RequestMapping(path = "/maven")
public class MavenController { public class MavenController {
private final StorageService storageService; private final MavenHandler mavenHandler;
public MavenController(@Qualifier("mirroredStorageService") StorageService storageService) { public MavenController(MavenHandler mavenHandler) {
this.storageService = storageService; this.mavenHandler = mavenHandler;
} }
@GetMapping(path = "/{*path}") @GetMapping(path = "/{*path}", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public ResponseEntity<Resource> mavenGetFile(@PathVariable("path") String httpPath) { public ResponseEntity<Resource> mavenGetFile(@PathVariable("path") String httpPath) {
ResponseEntity<Resource> responseEntity; return mavenHandler.getFile(httpPath.substring(1))
.<ResponseEntity<Resource>>map(response -> ResponseEntity.ok()
Optional<FileRecord> fileRecordOptional = storageService.getFile(httpPath);
if (fileRecordOptional.isEmpty()) {
responseEntity = ResponseEntity.notFound().build();
} else {
FileRecord fileRecord = fileRecordOptional.get();
responseEntity = ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, .header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + fileRecord.name() + "\"") "attachment; filename=\"" + response.name() + "\"")
.contentType(MediaType.APPLICATION_OCTET_STREAM) .contentType(MediaType.APPLICATION_OCTET_STREAM)
.contentLength(fileRecord.size()) .body(new InputStreamResource(response.inputStream())))
.body(new InputStreamResource(fileRecord.inputStream())); .orElseGet(() -> ResponseEntity.notFound().build());
}
return responseEntity;
} }
@SuppressWarnings("java:S1452") @SuppressWarnings("java:S1452")
@@ -57,7 +44,7 @@ public class MavenController {
HttpServletRequest httpRequest) throws IOException { HttpServletRequest httpRequest) throws IOException {
try (InputStream inputStream = httpRequest.getInputStream()) { try (InputStream inputStream = httpRequest.getInputStream()) {
storageService.putFile(httpPath, inputStream); mavenHandler.putFile(httpPath.substring(1), inputStream);
} }
return ResponseEntity.noContent().build(); return ResponseEntity.noContent().build();

View File

@@ -1,8 +1,28 @@
{ {
"properties": [ "properties": [
{ {
"name": "maven.storage", "name": "minio.url",
"type": "java.lang.String" "type": "java.lang.String",
"description": "MinIO console url."
},
{
"name": "minio.accessKey",
"type": "java.lang.String",
"description": "MinIO Access key (login)."
},
{
"name": "minio.secretKey",
"type": "java.lang.String",
"description": "MinIO Secret key (password)."
},
{
"name": "minio.bucket",
"type": "java.lang.String",
"description": "MinIO Bucket name."
},
{
"name": "maven-mirrors.list",
"type": "java.util.List<ru.di9.mirror.web.config.MavenMirrorsProperties.MirrorData>",
"sourceType": "java.util.List<ru.di9.mirror.web.config.MavenMirrorsProperties.MirrorData>"
} }
] ] }
}

View File

@@ -2,7 +2,22 @@ server:
address: 127.0.0.1 address: 127.0.0.1
port: 8080 port: 8080
spring:
freemarker:
template-loader-path: classpath:/templates
suffix: .ftl
debug: false debug: false
maven: minio:
storage: './storage' url: 'http://dev.di9.ru:9000'
accessKey: 'mirror'
secretKey: 'mirror123'
bucket: 'mirror'
maven-mirrors:
list:
- id: 'maven_central'
url: 'https://repo1.maven.org/maven2'
- id: 'spigot'
url: 'https://hub.spigotmc.org/nexus/content/groups/public'

View File

@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="ru">
<header>
<meta charset="UTF-8">
<title>Project-Mirror</title>
</header>
<body>
<h1>Index of</h1>
<hr>
<ul>
<#--noinspection FtlTypesInspection-->
<#list model["links"] as item>
<li><a href="${item.link()}">${item.name()}</a></li>
</#list>
</ul>
<hr>
<p style="text-align:right">Project-Mirror</p>
</body>
</html>

View File

@@ -1,4 +1,4 @@
rootProject.name = 'project-mirror' rootProject.name = 'project-mirror'
include('mirror-app') include('mirror-core')
include('mirror-web') include('mirror-web')

View File

@@ -1,7 +1,9 @@
ext { ext {
lombokVersion = '1.18.20' lombokVersion = '1.18.20'
okhttpVersion = '4.9.3' minioVersion = '8.3.9'
springBootVerson = '2.6.6'
pluginSpringBootVerson = '2.6.6' // for tests only
pluginSpringBomVerson = '1.0.11.RELEASE' junitJupiterVersion = '5.5.2'
mockitoCoreVersion = '4.5.1'
} }