отделили реализацию mongodb в отлдельный модуль
This commit is contained in:
@@ -2,5 +2,4 @@ apply plugin: 'java-library'
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api("com.squareup.okhttp3:okhttp:${okhttpVersion}")
|
api("com.squareup.okhttp3:okhttp:${okhttpVersion}")
|
||||||
api("org.mongodb:mongodb-driver-sync:${mongoDriver}")
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
package ru.di9.mirror.core.domain;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import java.util.StringJoiner;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
public record ArtifactRecord(String group, String artifactId, String version, boolean isSnapshot) {
|
|
||||||
private static final String SNAPSHOT = "SNAPSHOT";
|
|
||||||
|
|
||||||
public static ArtifactRecord fromHttpPath(String httpPath) {
|
|
||||||
if (httpPath.startsWith("/")) {
|
|
||||||
httpPath = httpPath.substring(1);
|
|
||||||
}
|
|
||||||
log.info("httpPath = {}", httpPath);
|
|
||||||
|
|
||||||
String[] partsPath = httpPath.split("/");
|
|
||||||
String version = partsPath[partsPath.length-2];
|
|
||||||
boolean isSnapshot = version.contains(SNAPSHOT);
|
|
||||||
|
|
||||||
String fileName = partsPath[partsPath.length-1];
|
|
||||||
|
|
||||||
String artifactId;
|
|
||||||
if (isSnapshot) {
|
|
||||||
String partVersion = version.substring(0, version.lastIndexOf(SNAPSHOT));
|
|
||||||
artifactId = fileName.substring(0, fileName.indexOf(partVersion) - 1);
|
|
||||||
} else {
|
|
||||||
artifactId = fileName.substring(0, (fileName.lastIndexOf('.') - version.length() - 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
StringJoiner sj = new StringJoiner(".");
|
|
||||||
for (int i = 0; i < partsPath.length - 3; i++) {
|
|
||||||
sj.add(partsPath[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ArtifactRecord(sj.toString(), artifactId, version, isSnapshot);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +1,49 @@
|
|||||||
package ru.di9.mirror.core.entity;
|
package ru.di9.mirror.core.entity;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.NoArgsConstructor;
|
||||||
import org.bson.types.ObjectId;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import ru.di9.mirror.core.domain.ArtifactRecord;
|
|
||||||
|
|
||||||
|
import java.util.StringJoiner;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(exclude = {"id"})
|
|
||||||
public class ArtifactEntity {
|
public class ArtifactEntity {
|
||||||
private ObjectId id;
|
private static final String SNAPSHOT = "SNAPSHOT";
|
||||||
|
|
||||||
private String group;
|
private String group;
|
||||||
private String artifactId;
|
private String artifactId;
|
||||||
private String version;
|
private String version;
|
||||||
private boolean isSnapshot;
|
private boolean isSnapshot;
|
||||||
|
|
||||||
public static ArtifactEntity fromRecord(ArtifactRecord artifactRecord) {
|
public static ArtifactEntity fromHttpPath(String httpPath) {
|
||||||
var entity = new ArtifactEntity();
|
if (httpPath.startsWith("/")) {
|
||||||
|
httpPath = httpPath.substring(1);
|
||||||
|
}
|
||||||
|
log.info("httpPath = {}", httpPath);
|
||||||
|
|
||||||
entity.setGroup(artifactRecord.group());
|
String[] partsPath = httpPath.split("/");
|
||||||
entity.setArtifactId(artifactRecord.artifactId());
|
String version = partsPath[partsPath.length-2];
|
||||||
entity.setVersion(artifactRecord.version());
|
boolean isSnapshot = version.contains(SNAPSHOT);
|
||||||
entity.setSnapshot(artifactRecord.isSnapshot());
|
|
||||||
|
|
||||||
return entity;
|
String fileName = partsPath[partsPath.length-1];
|
||||||
|
|
||||||
|
String artifactId;
|
||||||
|
if (isSnapshot) {
|
||||||
|
String partVersion = version.substring(0, version.lastIndexOf(SNAPSHOT));
|
||||||
|
artifactId = fileName.substring(0, fileName.indexOf(partVersion) - 1);
|
||||||
|
} else {
|
||||||
|
artifactId = fileName.substring(0, (fileName.lastIndexOf('.') - version.length() - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
StringJoiner sj = new StringJoiner(".");
|
||||||
|
for (int i = 0; i < partsPath.length - 3; i++) {
|
||||||
|
sj.add(partsPath[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ArtifactEntity(sj.toString(), artifactId, version, isSnapshot);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,12 @@ package ru.di9.mirror.core.handler;
|
|||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import ru.di9.mirror.core.domain.ArtifactRecord;
|
|
||||||
import ru.di9.mirror.core.domain.FileInfo;
|
import ru.di9.mirror.core.domain.FileInfo;
|
||||||
import ru.di9.mirror.core.entity.ArtifactEntity;
|
import ru.di9.mirror.core.entity.ArtifactEntity;
|
||||||
import ru.di9.mirror.core.handler.response.GetFileResponse;
|
import ru.di9.mirror.core.handler.response.GetFileResponse;
|
||||||
import ru.di9.mirror.core.repository.ArtifactRepository;
|
import ru.di9.mirror.core.repository.ArtifactRepository;
|
||||||
import ru.di9.mirror.core.service.ExternalMavenService;
|
|
||||||
import ru.di9.mirror.core.repository.FileStorageRepository;
|
import ru.di9.mirror.core.repository.FileStorageRepository;
|
||||||
|
import ru.di9.mirror.core.service.ExternalMavenService;
|
||||||
|
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -25,12 +24,12 @@ public class MavenHandler {
|
|||||||
public Optional<GetFileResponse> getFile(String path) {
|
public Optional<GetFileResponse> getFile(String path) {
|
||||||
var fileInfo = FileInfo.of(path.substring(path.lastIndexOf("/") + 1));
|
var fileInfo = FileInfo.of(path.substring(path.lastIndexOf("/") + 1));
|
||||||
|
|
||||||
ArtifactRecord artifactRecord = null;
|
ArtifactEntity artifactEntity = null;
|
||||||
if (!fileInfo.fullName().equalsIgnoreCase("maven-metadata.xml")
|
if (!fileInfo.fullName().equalsIgnoreCase("maven-metadata.xml")
|
||||||
&& !fileInfo.ext().equalsIgnoreCase("sha1")) {
|
&& !fileInfo.ext().equalsIgnoreCase("sha1")) {
|
||||||
|
|
||||||
artifactRecord = ArtifactRecord.fromHttpPath(path);
|
artifactEntity = ArtifactEntity.fromHttpPath(path);
|
||||||
log.info(artifactRecord.toString());
|
log.info(artifactEntity.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<InputStream> optionalInputStream = fileStorageRepository.findByName("/local/" + path);
|
Optional<InputStream> optionalInputStream = fileStorageRepository.findByName("/local/" + path);
|
||||||
@@ -38,11 +37,11 @@ public class MavenHandler {
|
|||||||
return optionalInputStream
|
return optionalInputStream
|
||||||
.map(inputStream -> new GetFileResponse(fileInfo.name(), inputStream));
|
.map(inputStream -> new GetFileResponse(fileInfo.name(), inputStream));
|
||||||
} else {
|
} else {
|
||||||
return findInMirrors(path, fileInfo.name(), artifactRecord);
|
return findInMirrors(path, fileInfo.name(), artifactEntity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Optional<GetFileResponse> findInMirrors(String path, String fileName, ArtifactRecord artifactRecord) {
|
private Optional<GetFileResponse> findInMirrors(String path, String fileName, ArtifactEntity artifactEntity) {
|
||||||
Optional<InputStream> result;
|
Optional<InputStream> result;
|
||||||
for (ExternalMavenService externalMavenService : externalMavenServices) {
|
for (ExternalMavenService externalMavenService : externalMavenServices) {
|
||||||
final String nameForStore = "/" + externalMavenService.getId() + "/" + path;
|
final String nameForStore = "/" + externalMavenService.getId() + "/" + path;
|
||||||
@@ -54,8 +53,8 @@ public class MavenHandler {
|
|||||||
} else {
|
} else {
|
||||||
result = externalMavenService.getFile(path);
|
result = externalMavenService.getFile(path);
|
||||||
if (result.isPresent()) {
|
if (result.isPresent()) {
|
||||||
if (artifactRecord != null) {
|
if (artifactEntity != null) {
|
||||||
repository.save(ArtifactEntity.fromRecord(artifactRecord));
|
repository.save(artifactEntity);
|
||||||
}
|
}
|
||||||
|
|
||||||
fileStorageRepository.save(nameForStore, result.get());
|
fileStorageRepository.save(nameForStore, result.get());
|
||||||
@@ -74,8 +73,8 @@ public class MavenHandler {
|
|||||||
if (!fileInfo.fullName().equalsIgnoreCase("maven-metadata.xml")
|
if (!fileInfo.fullName().equalsIgnoreCase("maven-metadata.xml")
|
||||||
&& !fileInfo.ext().equalsIgnoreCase("sha1")) {
|
&& !fileInfo.ext().equalsIgnoreCase("sha1")) {
|
||||||
|
|
||||||
ArtifactRecord artifactRecord = ArtifactRecord.fromHttpPath(path);
|
ArtifactEntity artifactEntity = ArtifactEntity.fromHttpPath(path);
|
||||||
log.info(artifactRecord.toString());
|
log.info(artifactEntity.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
fileStorageRepository.save("/local/" + path, inputStream);
|
fileStorageRepository.save("/local/" + path, inputStream);
|
||||||
|
|||||||
@@ -1,32 +1,8 @@
|
|||||||
package ru.di9.mirror.core.repository;
|
package ru.di9.mirror.core.repository;
|
||||||
|
|
||||||
import com.mongodb.client.MongoCollection;
|
|
||||||
import com.mongodb.client.result.InsertOneResult;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.bson.Document;
|
|
||||||
import org.bson.types.ObjectId;
|
|
||||||
import ru.di9.mirror.core.entity.ArtifactEntity;
|
import ru.di9.mirror.core.entity.ArtifactEntity;
|
||||||
|
|
||||||
@Slf4j
|
public interface ArtifactRepository {
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class ArtifactRepository {
|
|
||||||
private final MongoCollection<Document> collection;
|
|
||||||
|
|
||||||
public void save(ArtifactEntity artifactEntity) {
|
ArtifactEntity save(ArtifactEntity artifactEntity);
|
||||||
if (artifactEntity.getId() == null) {
|
|
||||||
insert(artifactEntity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void insert(ArtifactEntity artifactEntity) {
|
|
||||||
InsertOneResult insertOneResult = collection.insertOne(new Document()
|
|
||||||
.append("_id", new ObjectId())
|
|
||||||
.append("group", artifactEntity.getGroup())
|
|
||||||
.append("artifactId", artifactEntity.getArtifactId())
|
|
||||||
.append("version", artifactEntity.getVersion())
|
|
||||||
.append("is_snapshot", artifactEntity.isSnapshot()));
|
|
||||||
|
|
||||||
log.info("InsertedId: {}", insertOneResult.getInsertedId());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package ru.di9.mirror.core.domain;
|
package ru.di9.mirror.core.entity;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Assertions;
|
import org.junit.jupiter.api.Assertions;
|
||||||
import org.junit.jupiter.params.ParameterizedTest;
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
@@ -7,23 +7,23 @@ import org.junit.jupiter.params.provider.MethodSource;
|
|||||||
|
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
class ArtifactRecordTest {
|
class ArtifactEntityTest {
|
||||||
|
|
||||||
@ParameterizedTest(name = "[{index}] {0}")
|
@ParameterizedTest(name = "[{index}] {0}")
|
||||||
@MethodSource("fromHttpPathSource")
|
@MethodSource("fromHttpPathSource")
|
||||||
void fromHttpPath(String httpPath, ArtifactRecord expected) {
|
void fromHttpPath(String httpPath, ArtifactEntity expected) {
|
||||||
ArtifactRecord actual = ArtifactRecord.fromHttpPath(httpPath);
|
ArtifactEntity actual = ArtifactEntity.fromHttpPath(httpPath);
|
||||||
Assertions.assertEquals(expected, actual);
|
Assertions.assertEquals(expected, actual);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Stream<Arguments> fromHttpPathSource() {
|
static Stream<Arguments> fromHttpPathSource() {
|
||||||
return Stream.of(
|
return Stream.of(
|
||||||
Arguments.of("com/google/guava/guava/21.0/guava-21.0.jar",
|
Arguments.of("com/google/guava/guava/21.0/guava-21.0.jar",
|
||||||
new ArtifactRecord("com.google.guava", "guava", "21.0", false)),
|
new ArtifactEntity("com.google.guava", "guava", "21.0", false)),
|
||||||
Arguments.of("org/bukkit/bukkit/1.12.2-R0.1-SNAPSHOT/bukkit-1.12.2-R0.1-20180712.012114-155.jar",
|
Arguments.of("org/bukkit/bukkit/1.12.2-R0.1-SNAPSHOT/bukkit-1.12.2-R0.1-20180712.012114-155.jar",
|
||||||
new ArtifactRecord("org.bukkit", "bukkit", "1.12.2-R0.1-SNAPSHOT", true)),
|
new ArtifactEntity("org.bukkit", "bukkit", "1.12.2-R0.1-SNAPSHOT", true)),
|
||||||
Arguments.of("/org/projectlombok/lombok/1.18.22/lombok-1.18.22.jar",
|
Arguments.of("/org/projectlombok/lombok/1.18.22/lombok-1.18.22.jar",
|
||||||
new ArtifactRecord("org.projectlombok", "lombok", "1.18.22", false))
|
new ArtifactEntity("org.projectlombok", "lombok", "1.18.22", false))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
7
mirror-mongo/build.gradle
Normal file
7
mirror-mongo/build.gradle
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
apply plugin: 'java-library'
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(project(':mirror-core'))
|
||||||
|
|
||||||
|
api("org.mongodb:mongodb-driver-sync:${mongoDriver}")
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package ru.di9.mirror.mongo.entity;
|
||||||
|
|
||||||
|
import ru.di9.mirror.core.entity.ArtifactEntity;
|
||||||
|
|
||||||
|
public class MongoArtifactEntity extends ArtifactEntity {
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package ru.di9.mirror.mongo.repository;
|
||||||
|
|
||||||
|
import com.mongodb.client.MongoCollection;
|
||||||
|
import com.mongodb.client.result.InsertOneResult;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.bson.Document;
|
||||||
|
import org.bson.types.ObjectId;
|
||||||
|
import ru.di9.mirror.core.entity.ArtifactEntity;
|
||||||
|
import ru.di9.mirror.core.repository.ArtifactRepository;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MongoArtifactRepository implements ArtifactRepository {
|
||||||
|
private final MongoCollection<Document> collection;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ArtifactEntity save(ArtifactEntity artifactEntity) {
|
||||||
|
InsertOneResult insertOneResult = collection.insertOne(new Document()
|
||||||
|
.append("_id", new ObjectId())
|
||||||
|
.append("group", artifactEntity.getGroup())
|
||||||
|
.append("artifactId", artifactEntity.getArtifactId())
|
||||||
|
.append("version", artifactEntity.getVersion())
|
||||||
|
.append("is_snapshot", artifactEntity.isSnapshot()));
|
||||||
|
|
||||||
|
log.info("InsertedId: {}", insertOneResult.getInsertedId());
|
||||||
|
return artifactEntity;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ apply plugin: 'org.springframework.boot'
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(':mirror-core'))
|
implementation(project(':mirror-core'))
|
||||||
implementation(project(':mirror-minio'))
|
implementation(project(':mirror-minio'))
|
||||||
|
implementation(project(':mirror-mongo'))
|
||||||
|
|
||||||
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor:${springBootVerson}")
|
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor:${springBootVerson}")
|
||||||
implementation("org.springframework.boot:spring-boot-starter-web:${springBootVerson}")
|
implementation("org.springframework.boot:spring-boot-starter-web:${springBootVerson}")
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import ru.di9.mirror.minio.service.MinioRepository;
|
|||||||
public class MinioConfig {
|
public class MinioConfig {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public FileStorageRepository fileStorageRepository(MinioClient minioClient, @Value("${minio.bucket}") String bucket) {
|
public FileStorageRepository fileStorageRepository(MinioClient minioClient,
|
||||||
|
@Value("${minio.bucket}") String bucket) {
|
||||||
return new MinioRepository(minioClient, bucket);
|
return new MinioRepository(minioClient, bucket);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import org.bson.Document;
|
|||||||
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.core.repository.ArtifactRepository;
|
||||||
|
import ru.di9.mirror.mongo.repository.MongoArtifactRepository;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -17,7 +19,8 @@ import java.util.List;
|
|||||||
public class MongoConfig {
|
public class MongoConfig {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public MongoClient mongoClient(@Value("${mongodb.host}") String host, @Value("${mongodb.port}") int port) {
|
public MongoClient mongoClient(@Value("${mongodb.host}") String host,
|
||||||
|
@Value("${mongodb.port}") int port) {
|
||||||
return MongoClients.create(MongoClientSettings.builder()
|
return MongoClients.create(MongoClientSettings.builder()
|
||||||
.applyToClusterSettings(builder -> builder.hosts(List.of(new ServerAddress(host, port))))
|
.applyToClusterSettings(builder -> builder.hosts(List.of(new ServerAddress(host, port))))
|
||||||
.build());
|
.build());
|
||||||
@@ -33,4 +36,9 @@ public class MongoConfig {
|
|||||||
public MongoCollection<Document> artifactCollection(MongoDatabase database) {
|
public MongoCollection<Document> artifactCollection(MongoDatabase database) {
|
||||||
return database.getCollection("artifacts");
|
return database.getCollection("artifacts");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public ArtifactRepository artifactRepository(MongoCollection<Document> artifactCollection) {
|
||||||
|
return new MongoArtifactRepository(artifactCollection);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
package ru.di9.mirror.web.config;
|
package ru.di9.mirror.web.config;
|
||||||
|
|
||||||
import com.mongodb.client.MongoCollection;
|
|
||||||
import okhttp3.OkHttpClient;
|
import okhttp3.OkHttpClient;
|
||||||
import org.bson.Document;
|
|
||||||
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.core.handler.IndexOfHandler;
|
import ru.di9.mirror.core.handler.IndexOfHandler;
|
||||||
import ru.di9.mirror.core.handler.MavenHandler;
|
import ru.di9.mirror.core.handler.MavenHandler;
|
||||||
import ru.di9.mirror.core.repository.ArtifactRepository;
|
import ru.di9.mirror.core.repository.ArtifactRepository;
|
||||||
import ru.di9.mirror.core.service.ExternalMavenService;
|
|
||||||
import ru.di9.mirror.core.repository.FileStorageRepository;
|
import ru.di9.mirror.core.repository.FileStorageRepository;
|
||||||
|
import ru.di9.mirror.core.service.ExternalMavenService;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -41,8 +39,4 @@ public class WebConfig {
|
|||||||
return new OkHttpClient();
|
return new OkHttpClient();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
|
||||||
public ArtifactRepository artifactRepository(MongoCollection<Document> artifactCollection) {
|
|
||||||
return new ArtifactRepository(artifactCollection);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,4 +2,5 @@ rootProject.name = 'project-mirror'
|
|||||||
|
|
||||||
include('mirror-core')
|
include('mirror-core')
|
||||||
include('mirror-minio')
|
include('mirror-minio')
|
||||||
|
include('mirror-mongo')
|
||||||
include('mirror-web')
|
include('mirror-web')
|
||||||
|
|||||||
Reference in New Issue
Block a user