Skip to content

Commit

Permalink
Merge pull request #6 from asavershin/add-imagga
Browse files Browse the repository at this point in the history
Add imagga
  • Loading branch information
asavershin authored Jun 20, 2024
2 parents 24cc8ef + eb35948 commit 0769cb9
Show file tree
Hide file tree
Showing 20 changed files with 683 additions and 5 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ docker compose -f api/docker-compose.yml up --build -d

docker compose -f worker/docker-compose-blackwhite.yml up --build -d
docker compose -f worker/docker-compose-rotate.yml up --build -d
docker compose -f worker/docker-compose-imagga.yml up --build -d



13 changes: 13 additions & 0 deletions api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,19 @@
</properties>

<dependencies>

<dependency>
<groupId>com.bucket4j</groupId>
<artifactId>bucket4j-core</artifactId>
<version>8.10.1</version>
</dependency>

<dependency>
<groupId>com.bucket4j</groupId>
<artifactId>bucket4j-redis</artifactId>
<version>8.10.1</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.github.asavershin.api.config;

import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket;
import io.github.bucket4j.BucketConfiguration;
import io.github.bucket4j.distributed.ExpirationAfterWriteStrategy;
import io.github.bucket4j.distributed.proxy.ClientSideConfig;
import io.github.bucket4j.distributed.proxy.ProxyManager;
import io.github.bucket4j.redis.lettuce.cas.LettuceBasedProxyManager;
import io.lettuce.core.RedisClient;
import io.lettuce.core.RedisURI;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.codec.ByteArrayCodec;
import io.lettuce.core.codec.RedisCodec;
import io.lettuce.core.codec.StringCodec;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;

import java.time.Duration;
import java.util.function.Supplier;

@Configuration
public class Bucket4jConfig {
@Bean
public RedisClient redisClient(final RedisProperties redisProperties) {
return RedisClient.create(
RedisURI.Builder.redis(redisProperties.getHost(), redisProperties.getPort())
.withPassword(redisProperties.getPassword().toCharArray()).build());
}

@Bean
public ProxyManager<String> lettuceBasedProxyManager(final RedisClient redisClient) {
StatefulRedisConnection<String, byte[]> redisConnection = redisClient
.connect(RedisCodec.of(StringCodec.UTF8, ByteArrayCodec.INSTANCE));

return LettuceBasedProxyManager
.builderFor(redisConnection)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ public enum Filter {
/**
* Represents the CROP filter.
*/
BLACKWHITE;
BLACKWHITE,
/**
* Filter for external api imagga.
*/
IMAGGA;

/**
* Converts a string representation of a filter name string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,17 @@
import com.github.asavershin.api.infrastructure.in.controllers.dto.image.UploadImageResponse;
import com.github.asavershin.api.infrastructure.in.security.CustomUserDetails;
import com.github.asavershin.api.infrastructure.out.producers.KafkaProducer;
import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket;
import io.github.bucket4j.BucketConfiguration;
import io.github.bucket4j.distributed.proxy.ProxyManager;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
Expand All @@ -29,13 +36,16 @@
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.time.Duration;
import java.util.List;
import java.util.UUID;
import java.util.function.Supplier;

@RestController
@RequestMapping("/api/v1/image")
@Tag(name = "image", description = "Работа с изображениями")
@RequiredArgsConstructor
@Slf4j
public class ImageController {
/**
* Domain query that allows you take images of specific user.
Expand All @@ -54,6 +64,10 @@ public class ImageController {
* Service for tracking the status of image processing events.
*/
private final GetStatusEvent statusEvent;
/**
* Using for limiting requests to imagga filter.
*/
private final ProxyManager<String> pm;

/**
* Not final to allows Spring use proxy.
Expand Down Expand Up @@ -144,11 +158,12 @@ public byte[] downloadImage(
user.authenticatedUser().userId()
);
}

/**
* This start image processing event by authenticated user to his image.
*
* @param user Is param that injects by spring and contains
* current authenticated spring user.
* @param user Is param that injects by spring and contains
* current authenticated spring user.
* @param imageId The ID of the image that will be processed.
* @param filters The list of filters that will be applied to the image.
* @return request id
Expand All @@ -165,7 +180,24 @@ public ApplyImageFiltersResponse applyImageFilters(
.toList(),
ImageId.fromString(imageId)
);
producer.send(event, user.authenticatedUser().userId());
if (event.filters().contains(Filter.IMAGGA)) {
var b = getBucket(user);
var tokens = b.getAvailableTokens();
log.info("Available tokens {} for user {} ",
tokens,
user.getUsername()
);
if (tokens > 0) {
producer.send(event, user.authenticatedUser().userId());
b.tryConsume(1);
} else {
throw new RuntimeException(
"You have reached your limit on \"imagga\""
);
}
} else {
producer.send(event, user.authenticatedUser().userId());
}
return new ApplyImageFiltersResponse(
event.requestId().value().toString()
);
Expand Down Expand Up @@ -198,4 +230,19 @@ public ApplyImageFiltersResponse applyImageFilters(
)
);
}

private Bucket getBucket(final UserDetails userDetails) {
var username = userDetails.getUsername();
return pm.builder().build(
"bucket: " + username,
() -> BucketConfiguration.builder()
.addLimit(Bandwidth.builder().capacity(1L)
.refillIntervally(1,
Duration.ofDays(1L)
)
.build()
)
.build()
);
}
}
1 change: 1 addition & 0 deletions worker/.env
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ MINIO_CONSOLE_PORT=9090
MINIO_PORT=9000

# REDIS
REDIS_HOST_IMAGGA=redis-imagga
REDIS_HOST_ROTATE=redis-rotate
REDIS_HOST_BLACKWHITE=redis-blackwhite
REDIS_PORT=6379
Expand Down
7 changes: 7 additions & 0 deletions worker/Dockerfile-imagga
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
FROM openjdk:21

WORKDIR /app

COPY target/*.jar app.jar

CMD ["java", "-jar", "-Dspring.profiles.active=imagga", "app.jar"]
52 changes: 52 additions & 0 deletions worker/docker-compose-imagga.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
networks:
kafka-net:
name: kafka-net
driver: bridge
api-net:
name: api-net
driver: bridge

services:

backend-worker-imagga:
container_name: backend-worker-imagga
networks:
- kafka-net
- api-net
build:
context: .
dockerfile: Dockerfile-imagga
ports:
- 8084:8081
env_file:
- .env

redis-imagga:
networks:
- api-net
image: redis:7.2-rc-alpine
restart: always
container_name: redis-imagga
ports:
- '6381:6379'
command: redis-server --save 20 1 --loglevel debug --requirepass ${REDIS_PASSWORD}
volumes:
- redis-rotate-data:/data

# minio-api:
# networks:
# - api-net
# image: minio/minio:RELEASE.2024-02-14T21-36-02Z
# container_name: minio-api
# env_file:
# - .env
# command: server ~/minio --console-address :9090
# ports:
# - '9090:9090'
# - '9000:9000'
# volumes:
# - minio-api-data:/minio

volumes:
# minio-api-data:
redis-rotate-data:
19 changes: 19 additions & 0 deletions worker/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,25 @@
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</dependency>
<dependency>
<groupId>com.bucket4j</groupId>
<artifactId>bucket4j-core</artifactId>
<version>8.10.1</version>
</dependency>

<dependency>
<groupId>com.bucket4j</groupId>
<artifactId>bucket4j-redis</artifactId>
<version>8.10.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.github.asavershin.worker;

public class BadImaggaCodeException extends RuntimeException {
public BadImaggaCodeException(final String s) {
super(s);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.github.asavershin.worker.config;

import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket;
import io.github.bucket4j.BucketConfiguration;
import io.github.bucket4j.distributed.ExpirationAfterWriteStrategy;
import io.github.bucket4j.distributed.proxy.ClientSideConfig;
import io.github.bucket4j.distributed.proxy.ProxyManager;
import io.github.bucket4j.redis.lettuce.cas.LettuceBasedProxyManager;
import io.lettuce.core.RedisClient;
import io.lettuce.core.RedisURI;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.codec.ByteArrayCodec;
import io.lettuce.core.codec.RedisCodec;
import io.lettuce.core.codec.StringCodec;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.retry.annotation.EnableRetry;
import org.springframework.web.client.RestTemplate;

import java.time.Duration;

@Configuration
@EnableRetry
@Profile("imagga")
public class ImaggaConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}

@Bean
public RedisClient redisClient(final RedisProperties redisProperties) {
return RedisClient.create(
RedisURI.Builder.redis(redisProperties.getHost(), redisProperties.getPort())
.withPassword(redisProperties.getPassword().toCharArray()).build());
}

@Bean
public ProxyManager<String> lettuceBasedProxyManager(final RedisClient redisClient) {
StatefulRedisConnection<String, byte[]> redisConnection = redisClient
.connect(RedisCodec.of(StringCodec.UTF8, ByteArrayCodec.INSTANCE));

return LettuceBasedProxyManager
.builderFor(redisConnection)
.build();
}

@Bean
public Bucket bucketConfiguration(final ProxyManager<String> proxyManager) {
return proxyManager.builder().build(
"imagga",
() -> BucketConfiguration.builder()
.addLimit(
Bandwidth.builder()
.capacity(33)
.refillIntervally(
33,
Duration.ofDays(1L)
)
.build()
)
.build()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.github.asavershin.worker.config;

import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@Configuration
@ConfigurationProperties(prefix = "imagga")
@Getter
@Setter
public class ImaggaProperties {
private String key;
private String secret;
private String ttlprefix;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.github.asavershin.worker.dto;

import lombok.Data;
@Data
public class ImagaStatus {
private String text;
private String type;
}
Loading

0 comments on commit 0769cb9

Please sign in to comment.