├── .github
└── workflows
│ └── push.yml
├── .gitignore
├── LICENSE
├── Makefile
├── README.md
├── api
└── proto
│ └── v1
│ └── image_storage.proto
├── docker-compose.production.yaml
├── docker-compose.stress.yaml
├── docker-compose.yaml
├── images
├── alertmanager.jpeg
├── ci.png
├── diagram.png
├── gallery.png
├── grafana.jpeg
└── index.png
├── monitoring
├── alertmanager
│ └── alertmanager.yml
├── grafana
│ └── grafana.db
└── prometheus
│ ├── alert.rules
│ └── prometheus.yml
├── nginx
├── Dockerfile
├── nginx.conf
└── start.sh
├── pkg
├── database
│ └── postgres
│ │ └── postgres.go
├── go.mod
└── go.sum
├── scripts
├── database
│ ├── gateway
│ │ └── init.sql
│ └── history
│ │ └── init.sql
└── stress
│ ├── Dockerfile
│ ├── requirements.txt
│ └── stress_test.py
├── services
├── gateway
│ ├── Makefile
│ ├── cmd
│ │ └── main.go
│ ├── config
│ │ ├── config.go
│ │ └── gateway.yaml
│ ├── dev.Dockerfile
│ ├── go.mod
│ ├── go.sum
│ ├── monitoring
│ │ └── prometheus.go
│ ├── pb
│ │ ├── image_storage.pb.go
│ │ └── image_storage_grpc.pb.go
│ ├── prod.Dockerfile
│ ├── repository
│ │ └── images_postgres.go
│ ├── service
│ │ └── images.go
│ └── transport
│ │ ├── amqp
│ │ └── producer.go
│ │ └── rest
│ │ ├── handler
│ │ └── images.go
│ │ └── server.go
├── history
│ ├── Makefile
│ ├── cmd
│ │ └── main.go
│ ├── config
│ │ ├── config.go
│ │ └── history.yaml
│ ├── dev.Dockerfile
│ ├── go.mod
│ ├── go.sum
│ ├── prod.Dockerfile
│ ├── repository
│ │ └── notifications_postgres.go
│ ├── service
│ │ └── notifications.go
│ └── transport
│ │ └── amqp
│ │ └── consumer.go
└── storage
│ ├── Makefile
│ ├── aws
│ └── aws.go
│ ├── cmd
│ └── main.go
│ ├── config
│ ├── config.go
│ └── storage.yaml
│ ├── dev.Dockerfile
│ ├── go.mod
│ ├── go.sum
│ ├── pb
│ ├── image_storage.pb.go
│ └── image_storage_grpc.pb.go
│ ├── prod.Dockerfile
│ └── transport
│ └── grpc
│ ├── handler
│ └── handler.go
│ └── server.go
└── website
├── css
└── style.css
├── images
└── favicon.ico
├── index.html
└── js
└── script.js
/.github/workflows/push.yml:
--------------------------------------------------------------------------------
1 | name: "Push to Yandex Cloud CR"
2 | on:
3 | push:
4 | branches: [ main ]
5 |
6 | jobs:
7 | check:
8 | name: Check changed files
9 | outputs:
10 | gateway_service: ${{ steps.check_files.outputs.gateway_service }}
11 | history_service: ${{ steps.check_files.outputs.history_service }}
12 | storage_service: ${{ steps.check_files.outputs.storage_service }}
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: Checkout code
16 | uses: actions/checkout@v3
17 | with:
18 | fetch-depth: 2
19 |
20 | - name: Check modified files
21 | id: check_files
22 | run: |
23 | echo "=============== list modified files ==============="
24 | git diff --name-only HEAD^ HEAD
25 |
26 | echo "========== check paths of modified files =========="
27 | git diff --name-only HEAD^ HEAD > files.txt
28 | while IFS= read -r file; do
29 | echo $file
30 | if [[ $file == services/gateway/* ]]; then
31 | echo "::set-output name=gateway_service::true"
32 | elif [[ $file == services/history/* ]]; then
33 | echo "::set-output name=history_service::true"
34 | elif [[ $file == services/storage/* ]]; then
35 | echo "::set-output name=storage_service::true"
36 | else
37 | echo "file does not belong to any service"
38 | fi
39 | done < files.txt
40 |
41 | gateway_service:
42 | needs: check
43 | if: needs.check.outputs.gateway_service == 'true'
44 | runs-on: ubuntu-latest
45 | steps:
46 | - name: Checkout code
47 | uses: actions/checkout@v3
48 |
49 | - name: Login to Yandex Cloud Container Registry
50 | id: login-cr
51 | uses: yc-actions/yc-cr-login@v1
52 | with:
53 | yc-sa-json-credentials: ${{ secrets.YC_SA_JSON_CREDENTIALS }}
54 |
55 | - name: Build, tag, and push image to Yandex Cloud Container Registry
56 | env:
57 | CR_REGISTRY: crpb45qs3j62nc2j1lts
58 | CR_REPOSITORY: gateway
59 | IMAGE_TAG: ${{ github.sha }}
60 | PATH_TO_DOCKERFILE: services/gateway
61 | run: |
62 | docker build -t cr.yandex/$CR_REGISTRY/$CR_REPOSITORY:$IMAGE_TAG --file $PATH_TO_DOCKERFILE/prod.Dockerfile $PATH_TO_DOCKERFILE
63 | docker push cr.yandex/$CR_REGISTRY/$CR_REPOSITORY:$IMAGE_TAG
64 |
65 | history_service:
66 | needs: check
67 | if: needs.check.outputs.history_service == 'true'
68 | runs-on: ubuntu-latest
69 | steps:
70 | - name: Checkout code
71 | uses: actions/checkout@v3
72 |
73 | - name: Login to Yandex Cloud Container Registry
74 | id: login-cr
75 | uses: yc-actions/yc-cr-login@v1
76 | with:
77 | yc-sa-json-credentials: ${{ secrets.YC_SA_JSON_CREDENTIALS }}
78 |
79 | - name: Build, tag, and push image to Yandex Cloud Container Registry
80 | env:
81 | CR_REGISTRY: crpb45qs3j62nc2j1lts
82 | CR_REPOSITORY: history
83 | IMAGE_TAG: ${{ github.sha }}
84 | PATH_TO_DOCKERFILE: services/history
85 | run: |
86 | docker build -t cr.yandex/$CR_REGISTRY/$CR_REPOSITORY:$IMAGE_TAG --file $PATH_TO_DOCKERFILE/prod.Dockerfile $PATH_TO_DOCKERFILE
87 | docker push cr.yandex/$CR_REGISTRY/$CR_REPOSITORY:$IMAGE_TAG
88 |
89 | storage_service:
90 | needs: check
91 | if: needs.check.outputs.storage_service == 'true'
92 | runs-on: ubuntu-latest
93 | steps:
94 | - name: Checkout code
95 | uses: actions/checkout@v3
96 |
97 | - name: Login to Yandex Cloud Container Registry
98 | id: login-cr
99 | uses: yc-actions/yc-cr-login@v1
100 | with:
101 | yc-sa-json-credentials: ${{ secrets.YC_SA_JSON_CREDENTIALS }}
102 |
103 | - name: Build, tag, and push image to Yandex Cloud Container Registry
104 | env:
105 | CR_REGISTRY: crpb45qs3j62nc2j1lts
106 | CR_REPOSITORY: storage
107 | IMAGE_TAG: ${{ github.sha }}
108 | PATH_TO_DOCKERFILE: services/storage
109 | run: |
110 | docker build -t cr.yandex/$CR_REGISTRY/$CR_REPOSITORY:$IMAGE_TAG --file $PATH_TO_DOCKERFILE/prod.Dockerfile $PATH_TO_DOCKERFILE
111 | docker push cr.yandex/$CR_REGISTRY/$CR_REPOSITORY:$IMAGE_TAG
112 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | */.DS_Store
3 |
4 | services/gateway/.bin
5 | services/gateway/.env
6 |
7 | services/history/.bin
8 | services/history/.env
9 |
10 | services/storage/.bin
11 | services/storage/.env
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Alexey Fedoseev
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | ifeq ($(version), prod)
2 | DOCKER_COMPOSE_FILE = -f docker-compose.production.yaml
3 | else ifeq ($(version), stress)
4 | DOCKER_COMPOSE_FILE = -f docker-compose.yaml -f docker-compose.stress.yaml
5 | else
6 | DOCKER_COMPOSE_FILE = -f docker-compose.yaml
7 | endif
8 |
9 | up:
10 | docker-compose ${DOCKER_COMPOSE_FILE} up --build
11 |
12 | down:
13 | docker-compose ${DOCKER_COMPOSE_FILE} down
14 |
15 | ps:
16 | docker-compose ${DOCKER_COMPOSE_FILE} ps
17 |
18 | re: down up
19 |
20 | .DEFAULT_GOAL := re
21 | .PHONY: up down ps re
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Lightstream
6 |
7 | Photo hosting microservice application
8 |
9 | Report Bug
10 | ·
11 | Request Feature
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | Table of Contents
22 |
23 | About The Project
24 |
29 |
30 | Getting Started
31 |
34 |
35 | Usage
36 | Roadmap
37 | Contributing
38 | Contact
39 |
40 |
41 |
42 |
43 | ## About The Project
44 |
45 | This project is a photo hosting implementation aimed at learning microservice architecture and devops culture.
46 |
47 | Technologies used:
48 | * [Golang](https://go.dev/)
49 | * [PostgreSQL](https://www.postgresql.org/)
50 | * [gRPC](https://grpc.io/), [REST](https://ru.wikipedia.org/wiki/REST), [AMQP](https://ru.wikipedia.org/wiki/AMQP) ([RabbitMQ](https://www.rabbitmq.com/))
51 | * [Docker](https://www.docker.com/), [CI/CD](https://ru.wikipedia.org/wiki/CI/CD), [Github Actions](https://github.com/features/actions), [Terraform](https://www.terraform.io/), [Kubernetes](https://kubernetes.io/)
52 | * [Amazon S3](https://aws.amazon.com/s3/), [Yandex Cloud](https://cloud.yandex.com/en-ru/)
53 | * [NGINX](https://nginx.org/)
54 | * [Prometheus](https://prometheus.io/), [Grafana](https://grafana.com/)
55 | * [Python](https://www.python.org/), [JS](https://developer.mozilla.org/en-US/docs/Web/JavaScript), [HTML](https://developer.mozilla.org/en-US/docs/Web/HTML), [CSS](https://developer.mozilla.org/en-US/docs/Web/CSS)
56 |
57 |
58 | ### Project Structure
59 | The project consists of three microservices: ***gateway***, ***history***, ***storage***:
60 | - ***gateway*** microservice is the central part of the application. All other parts of the application are associated with this service.
61 | - ***history*** microservice saves the history of image requests.
62 | - ***storage*** microservice communicates with Yandex cloud and saves pictures there.
63 |
64 |
65 | (back to top )
66 |
67 |
68 | ### Monitoring
69 | The project is configured to monitor the system with Prometheus and Grafana.
70 | (Grafana - `http://localhost:3030`)
71 |
72 | [4 Golden Signals](https://sre.google/sre-book/monitoring-distributed-systems/) are used to monitor the application
73 |
74 |
75 |
76 | If the server crashes or the load is too high, the Alertmanager will send a notification in Telegram.
77 |
78 |
79 |
80 |
81 |
82 |
83 | (back to top )
84 |
85 |
86 | ### Continuous Integration
87 | Continuous Integration pipeline set up in the project.
88 | In the case of a push to the main branch, Docker Images of changed microservices are rebuilt and sent to the [Yandex Cloud Registry](https://cloud.yandex.com/en-ru/docs/container-registry/)
89 |
90 |
91 |
92 | (back to top )
93 |
94 |
95 | ## Getting Started
96 |
97 | This is an example of how you may give instructions on setting up your project locally.
98 | To get a local copy up and running follow these example steps.
99 |
100 | ### Installation
101 |
102 | 1. Clone the repo
103 | ```sh
104 | git clone https://github.com/rvinnie/lightstream
105 | ```
106 | 2. Set environment variables
107 | - `services/gateway/.env`
108 | ```sh
109 | POSTGRES_USER=
110 | POSTGRES_PASSWORD=
111 | POSTGRES_DB=
112 | DATABASE_HOST=postgres_gateway
113 |
114 | RABBIT_USER=
115 | RABBIT_PASSWORD=
116 |
117 | GIN_MODE=debug
118 | ```
119 | - `services/history/.env`
120 | ```sh
121 | POSTGRES_USER=
122 | POSTGRES_PASSWORD=
123 | POSTGRES_DB=
124 | DATABASE_HOST=postgres_history
125 |
126 | RABBIT_USER=
127 | RABBIT_PASSWORD=
128 | ```
129 | - `services/history/.env`
130 | ```sh
131 | AWS_ACCESS_KEY_ID=
132 | AWS_SECRET_ACCESS_KEY=
133 | AWS_REGION=ru-central1
134 | ```
135 | - `monitoring/alertmanager/alertmanager.yml`
136 | ```yml
137 | ...
138 | - bot_token:
139 | api_url: 'https://api.telegram.org'
140 | chat_id:
141 | ...
142 | ```
143 | - `.github/workflows/push.yml`
144 | Put `YC_SA_JSON_CREDENTIALS` in Github Actions secrets.
145 | 3. Make sure [docker](https://www.docker.com/) is installed
146 | 4. Choose one of the three versions of the project and run
147 | - development version
148 | ```sh
149 | make
150 | ```
151 | - development version with stress testing
152 | ```sh
153 | make version=stress
154 | ```
155 | - production version
156 | ```sh
157 | make version=prod
158 | ```
159 |
160 | (back to top )
161 |
162 |
163 | ## Usage
164 |
165 | After launch, go to the address in the browser
166 | `https://localhost`
167 |
168 |
169 |
170 | Here you can add an image to storage, find an image by id and get the whole gallery of images.
171 | All added images are saved in Yandex Cloud Object Storage.
172 |
173 | An example of a gallery with two images (`Upload` two images -> press `Get all`)
174 |
175 |
176 | (back to top )
177 |
178 |
179 | ## Roadmap
180 |
181 | - [ ] Send image by link
182 | - [ ] Add user entity
183 | - [ ] Add authorization microservice (JWT)
184 | - [ ] Add Terraform
185 | - [ ] Add Kubernetes
186 |
187 |
188 | See the [open issues](https://github.com/rvinnie/lightstream/issues) for a full list of proposed features (and known issues).
189 |
190 | (back to top )
191 |
192 |
193 | ## Contributing
194 |
195 | Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.
196 |
197 | If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement".
198 | Don't forget to give the project a star! Thanks again!
199 |
200 | 1. Fork the Project
201 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
202 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
203 | 4. Push to the Branch (`git push origin feature/AmazingFeature`)
204 | 5. Open a Pull Request
205 |
206 | (back to top )
207 |
208 |
209 | ## Contact
210 |
211 | Alexey Fedoseev - [@fedoseev_alexey](https://t.me/fedoseev_alexey) - rv1nnie@yandex.ru
212 |
213 | Project Link: [Lightstream](https://github.com/rvinnie/lightstream)
214 |
215 | (back to top )
216 |
--------------------------------------------------------------------------------
/api/proto/v1/image_storage.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | import "google/protobuf/empty.proto";
4 |
5 | option go_package = "github.com/rvinnie/lightstream/api/proto/v1";
6 |
7 | package gateway;
8 |
9 | service ImageStorage {
10 | rpc CreateImage (CreateImageRequest) returns (google.protobuf.Empty) {}
11 | rpc GetImage (FindImageRequest) returns (FindImageResponse) {}
12 | rpc GetImages (FindImagesRequest) returns (FindImagesResponse) {}
13 | }
14 |
15 | message CreateImageRequest {
16 | string path = 1;
17 | string contentType = 2;
18 | bytes image = 3;
19 | }
20 |
21 | message FindImageRequest {
22 | string path = 1;
23 | }
24 |
25 | message FindImagesRequest {
26 | repeated string paths = 1;
27 | }
28 |
29 | message FindImageResponse {
30 | string name = 1;
31 | string contentType = 2;
32 | bytes image = 3;
33 | }
34 |
35 | message FindImagesResponse {
36 | repeated FindImageResponse images = 1;
37 | }
--------------------------------------------------------------------------------
/docker-compose.production.yaml:
--------------------------------------------------------------------------------
1 | version: '3.9'
2 | services:
3 | nginx:
4 | image: nginx
5 | build:
6 | context: ./nginx
7 | dockerfile: Dockerfile
8 | container_name: nginx
9 | ports:
10 | - "80:80"
11 | - "443:443"
12 | volumes:
13 | - ./nginx/nginx.conf:/etc/nginx/nginx.conf
14 | - ./website:/var/www/lightstream/website
15 | restart: always
16 |
17 | postgres_gateway:
18 | image: postgres:15.3-alpine
19 | container_name: postgres_gateway
20 | ports:
21 | - "5432:5432"
22 | env_file:
23 | - ./services/gateway/.env
24 | volumes:
25 | - ./scripts/database/gateway/init.sql:/docker-entrypoint-initdb.d/create_tables.sql
26 | restart: always
27 |
28 | postgres_history:
29 | image: postgres:15.3-alpine
30 | container_name: postgres_history
31 | ports:
32 | - "5433:5432"
33 | env_file:
34 | - ./services/history/.env
35 | volumes:
36 | - ./scripts/database/history/init.sql:/docker-entrypoint-initdb.d/create_tables.sql
37 | restart: always
38 |
39 | rabbit:
40 | image: rabbitmq:3.11.17-management-alpine
41 | container_name: rabbit
42 | ports:
43 | - "5672:5672"
44 | - "15672:15672"
45 | restart: always
46 |
47 | storage:
48 | image: storage
49 | build:
50 | context: ./services/storage
51 | dockerfile: prod.Dockerfile
52 | container_name: storage
53 | ports:
54 | - "4040:4040"
55 | env_file:
56 | - ./services/storage/.env
57 | volumes:
58 | - ./services/storage/:/usr/src/app
59 | restart: "no"
60 |
61 | history:
62 | image: history
63 | build:
64 | context: ./services/history
65 | dockerfile: prod.Dockerfile
66 | container_name: history
67 | env_file:
68 | - ./services/history/.env
69 | volumes:
70 | - ./services/history/:/usr/src/app
71 | depends_on:
72 | - rabbit
73 | - postgres_history
74 | restart: "no"
75 |
76 | gateway:
77 | image: gateway
78 | build:
79 | context: ./services/gateway
80 | dockerfile: prod.Dockerfile
81 | container_name: gateway
82 | ports:
83 | - "8080:8080"
84 | env_file:
85 | - ./services/gateway/.env
86 | volumes:
87 | - ./services/gateway/:/usr/src/app
88 | depends_on:
89 | - postgres_gateway
90 | - rabbit
91 | - storage
92 | - history
93 | restart: "no"
94 |
95 | prometheus:
96 | image: prom/prometheus:v2.42.0
97 | container_name: prometheus
98 | ports:
99 | - "9090:9090"
100 | volumes:
101 | - ./monitoring/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
102 | - ./monitoring/prometheus/alert.rules:/etc/prometheus/alert.rules
103 | depends_on:
104 | - gateway
105 | restart: always
106 |
107 | grafana:
108 | image: grafana/grafana:9.3.6
109 | container_name: grafana
110 | ports:
111 | - "3000:3000"
112 | volumes:
113 | - ./monitoring/grafana/grafana.db:/var/lib/grafana/grafana.db
114 | depends_on:
115 | - prometheus
116 | restart: always
117 |
118 | alertmanager:
119 | image: prom/alertmanager
120 | container_name: alertmanager
121 | ports:
122 | - "9093:9093"
123 | volumes:
124 | - ./monitoring/alertmanager/alertmanager.yml:/etc/alertmanager/alertmanager.yml
125 | command:
126 | - '--config.file=/etc/alertmanager/alertmanager.yml'
127 | depends_on:
128 | - prometheus
129 | restart: always
--------------------------------------------------------------------------------
/docker-compose.stress.yaml:
--------------------------------------------------------------------------------
1 | version: '3.9'
2 | services:
3 | stress:
4 | image: stress
5 | build:
6 | context: ./scripts/stress
7 | dockerfile: Dockerfile
8 | container_name: stress
9 | volumes:
10 | - ./scripts/stress:/usr/src/app
11 | depends_on:
12 | - gateway
13 | restart: always
14 |
--------------------------------------------------------------------------------
/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | version: '3.9'
2 | services:
3 | nginx:
4 | image: nginx
5 | build:
6 | context: ./nginx
7 | dockerfile: Dockerfile
8 | container_name: nginx
9 | ports:
10 | - "80:80"
11 | - "443:443"
12 | volumes:
13 | - ./nginx/nginx.conf:/etc/nginx/nginx.conf
14 | - ./website:/var/www/lightstream/website
15 | restart: always
16 |
17 | postgres_gateway:
18 | image: postgres:15.3-alpine
19 | container_name: postgres_gateway
20 | ports:
21 | - "5432:5432"
22 | env_file:
23 | - ./services/gateway/.env
24 | volumes:
25 | - ./scripts/database/gateway/init.sql:/docker-entrypoint-initdb.d/create_tables.sql
26 | restart: always
27 |
28 | postgres_history:
29 | image: postgres:15.3-alpine
30 | container_name: postgres_history
31 | ports:
32 | - "5433:5432"
33 | env_file:
34 | - ./services/history/.env
35 | volumes:
36 | - ./scripts/database/history/init.sql:/docker-entrypoint-initdb.d/create_tables.sql
37 | restart: always
38 |
39 | rabbit:
40 | image: rabbitmq:3.11.17-management-alpine
41 | container_name: rabbit
42 | ports:
43 | - "5672:5672"
44 | - "15672:15672"
45 | restart: always
46 |
47 | storage:
48 | image: storage
49 | build:
50 | context: ./services/storage
51 | dockerfile: dev.Dockerfile
52 | container_name: storage
53 | ports:
54 | - "4040:4040"
55 | env_file:
56 | - ./services/storage/.env
57 | volumes:
58 | - ./services/storage/:/usr/src/app
59 | restart: "no"
60 |
61 | history:
62 | image: history
63 | build:
64 | context: ./services/history
65 | dockerfile: dev.Dockerfile
66 | container_name: history
67 | env_file:
68 | - ./services/history/.env
69 | volumes:
70 | - ./services/history/:/usr/src/app
71 | depends_on:
72 | - rabbit
73 | - postgres_history
74 | restart: "no"
75 |
76 | gateway:
77 | image: gateway
78 | build:
79 | context: ./services/gateway
80 | dockerfile: dev.Dockerfile
81 | container_name: gateway
82 | ports:
83 | - "8080:8080"
84 | env_file:
85 | - ./services/gateway/.env
86 | volumes:
87 | - ./services/gateway/:/usr/src/app
88 | depends_on:
89 | - postgres_gateway
90 | - rabbit
91 | - storage
92 | - history
93 | restart: "no"
94 |
95 | prometheus:
96 | image: prom/prometheus:v2.42.0
97 | container_name: prometheus
98 | ports:
99 | - "9090:9090"
100 | volumes:
101 | - ./monitoring/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
102 | - ./monitoring/prometheus/alert.rules:/etc/prometheus/alert.rules
103 | depends_on:
104 | - gateway
105 | restart: always
106 |
107 | grafana:
108 | image: grafana/grafana:9.3.6
109 | container_name: grafana
110 | ports:
111 | - "3000:3000"
112 | volumes:
113 | - ./monitoring/grafana/grafana.db:/var/lib/grafana/grafana.db
114 | depends_on:
115 | - prometheus
116 | restart: always
117 |
118 | alertmanager:
119 | image: prom/alertmanager
120 | container_name: alertmanager
121 | ports:
122 | - "9093:9093"
123 | volumes:
124 | - ./monitoring/alertmanager/alertmanager.yml:/etc/alertmanager/alertmanager.yml
125 | command:
126 | - '--config.file=/etc/alertmanager/alertmanager.yml'
127 | depends_on:
128 | - prometheus
129 | restart: always
130 |
--------------------------------------------------------------------------------
/images/alertmanager.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rvinnie/lightstream/69dedca6bc746ef456b14462416ceedec4beb711/images/alertmanager.jpeg
--------------------------------------------------------------------------------
/images/ci.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rvinnie/lightstream/69dedca6bc746ef456b14462416ceedec4beb711/images/ci.png
--------------------------------------------------------------------------------
/images/diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rvinnie/lightstream/69dedca6bc746ef456b14462416ceedec4beb711/images/diagram.png
--------------------------------------------------------------------------------
/images/gallery.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rvinnie/lightstream/69dedca6bc746ef456b14462416ceedec4beb711/images/gallery.png
--------------------------------------------------------------------------------
/images/grafana.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rvinnie/lightstream/69dedca6bc746ef456b14462416ceedec4beb711/images/grafana.jpeg
--------------------------------------------------------------------------------
/images/index.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rvinnie/lightstream/69dedca6bc746ef456b14462416ceedec4beb711/images/index.png
--------------------------------------------------------------------------------
/monitoring/alertmanager/alertmanager.yml:
--------------------------------------------------------------------------------
1 | global:
2 | resolve_timeout: 10s
3 |
4 | route:
5 | group_by: ['alertname', 'service']
6 | group_wait: 3s
7 | group_interval: 10s
8 | receiver: 'telegram_bot'
9 |
10 | receivers:
11 | - name: 'telegram_bot'
12 | telegram_configs:
13 | - bot_token: BOT_TOKEN_STRING
14 | api_url: 'https://api.telegram.org'
15 | chat_id: CHAT_ID_INT
16 | send_resolved: true
17 | parse_mode: ''
18 | message: "<< {{ .GroupLabels.alertname }} >>\n🔥 Severity: {{ .CommonLabels.severity }}\n📖 Info: {{ range .Alerts }}{{ .Annotations.description }}\n{{ end }}"
19 |
20 | templates:
21 | - '/etc/alertmanager/templates/*.tmpl'
--------------------------------------------------------------------------------
/monitoring/grafana/grafana.db:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rvinnie/lightstream/69dedca6bc746ef456b14462416ceedec4beb711/monitoring/grafana/grafana.db
--------------------------------------------------------------------------------
/monitoring/prometheus/alert.rules:
--------------------------------------------------------------------------------
1 | groups:
2 | - name: alert.rules
3 | rules:
4 | - alert: InstanceDown
5 | expr: up == 0
6 | for: 5m
7 | labels:
8 | severity: critical
9 | annotations:
10 | summary: "Instance down"
11 | description: "Server {{ $labels.instance }} down"
12 | - alert: APIHighRequestLatency
13 | expr: sum(rate(gateway_requests_duration_sum{code=~"2..", url!="/metrics"}[15s])) by (url) / sum(rate(gateway_requests_duration_count{code=~"2..", url!="/metrics"}[15s])) by (url) * 1000 > 1000
14 | for: 1m
15 | labels:
16 | severity: warning
17 | annotations:
18 | summary: "High request latency on {{ $labels.instance }}"
19 | description: "server has a median request latency above 1s"
20 | - alert: APIHighSaturation
21 | expr: http_concurrent_requests / http_concurrent_requests_max * 100 > 75
22 | for: 1m
23 | labels:
24 | severity: critical
25 | annotations:
26 | summary: "High saturation on {{ $labels.instance }}"
27 | description: "server has a median saturation above 75% (current value: {{ $value }}%)"
28 |
--------------------------------------------------------------------------------
/monitoring/prometheus/prometheus.yml:
--------------------------------------------------------------------------------
1 | global:
2 | scrape_interval: 5s
3 | evaluation_interval: 5s
4 |
5 | alerting:
6 | alertmanagers:
7 | - scheme: http
8 | static_configs:
9 | - targets:
10 | - "alertmanager:9093"
11 |
12 | rule_files:
13 | - 'alert.rules'
14 |
15 | scrape_configs:
16 | - job_name: gateway
17 | static_configs:
18 | - targets:
19 | - "gateway:8080"
--------------------------------------------------------------------------------
/nginx/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM alpine:3.11.5
2 |
3 | ENV NGINX_VERSION=1.16.1-r8
4 |
5 | EXPOSE 80 443
6 |
7 | RUN apk add --no-cache nginx=${NGINX_VERSION} \
8 | && apk add openssl \
9 | && mkdir -p /run/nginx \
10 | && mkdir -p /var/www/lightstream/website
11 |
12 | COPY ./start.sh /var/www
13 |
14 | RUN sh /var/www/start.sh
15 |
16 | CMD nginx -g 'daemon off;';
17 |
--------------------------------------------------------------------------------
/nginx/nginx.conf:
--------------------------------------------------------------------------------
1 | events {}
2 |
3 | http {
4 | ssl_certificate /etc/ssl/certs/nginx-selfsigned.crt;
5 | ssl_certificate_key /etc/ssl/private/nginx-selfsigned.key;
6 |
7 | access_log /var/log/nginx/lightstream_access.log;
8 | error_log /var/log/nginx/lightstream_error.log;
9 |
10 | include /etc/nginx/mime.types;
11 |
12 | server {
13 | listen 80;
14 | listen [::]:80;
15 |
16 | server_name localhost;
17 |
18 | return 301 https://$server_name$request_uri;
19 | }
20 |
21 | server {
22 | listen 443 ssl;
23 | listen [::]:443 ssl;
24 |
25 | server_name localhost;
26 |
27 | root /var/www/lightstream/website;
28 |
29 | location / {
30 | root /var/www/lightstream/website;
31 | index index.html;
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/nginx/start.sh:
--------------------------------------------------------------------------------
1 | if [ ! -f /etc/ssl/certs/nginx.crt ]; then
2 | echo "Nginx: setting up ssl ...";
3 | openssl req -x509 -nodes -days 365 -newkey rsa:4096 \
4 | -keyout /etc/ssl/private/nginx-selfsigned.key \
5 | -out /etc/ssl/certs/nginx-selfsigned.crt \
6 | -subj "/C=RU/ST=Moscow/L=Moscow/O=lightstream/CN=lightstream.ru";
7 | echo "Nginx: ssl is set up!";
8 | fi
9 |
--------------------------------------------------------------------------------
/pkg/database/postgres/postgres.go:
--------------------------------------------------------------------------------
1 | package postgres
2 |
3 | import (
4 | "context"
5 | "github.com/jackc/pgx/v5/pgxpool"
6 | "net"
7 | "net/url"
8 | )
9 |
10 | type DBConfig struct {
11 | Username string
12 | Password string
13 | Host string
14 | Port string
15 | DBName string
16 | }
17 |
18 | func NewConnPool(dbConfig DBConfig) (*pgxpool.Pool, error) {
19 |
20 | databaseUrl := formUrl("postgres", dbConfig.Username, dbConfig.Password, dbConfig.Host, dbConfig.Port, dbConfig.DBName)
21 | dbPool, err := pgxpool.New(context.Background(), databaseUrl)
22 | if err != nil {
23 | return nil, err
24 | }
25 |
26 | err = dbPool.Ping(context.Background())
27 | if err != nil {
28 | return nil, err
29 | }
30 |
31 | return dbPool, err
32 | }
33 |
34 | func formUrl(scheme, username, password, host, port, path string) string {
35 | var u = url.URL{
36 | Scheme: scheme,
37 | User: url.UserPassword(username, password),
38 | Host: net.JoinHostPort(host, port),
39 | Path: path,
40 | }
41 | return u.String()
42 | }
43 |
--------------------------------------------------------------------------------
/pkg/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/rvinnie/lightstream/pkg
2 |
3 | go 1.19
4 |
5 | require github.com/jackc/pgx/v5 v5.3.1
6 |
7 | require (
8 | github.com/jackc/pgpassfile v1.0.0 // indirect
9 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
10 | github.com/jackc/puddle/v2 v2.2.0 // indirect
11 | golang.org/x/crypto v0.6.0 // indirect
12 | golang.org/x/sync v0.1.0 // indirect
13 | golang.org/x/text v0.7.0 // indirect
14 | )
15 |
--------------------------------------------------------------------------------
/pkg/go.sum:
--------------------------------------------------------------------------------
1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
3 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
4 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
5 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
6 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
7 | github.com/jackc/pgx/v5 v5.3.1 h1:Fcr8QJ1ZeLi5zsPZqQeUZhNhxfkkKBOgJuYkJHoBOtU=
8 | github.com/jackc/pgx/v5 v5.3.1/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8=
9 | github.com/jackc/puddle/v2 v2.2.0 h1:RdcDk92EJBuBS55nQMMYFXTxwstHug4jkhT5pq8VxPk=
10 | github.com/jackc/puddle/v2 v2.2.0/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
11 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
12 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
13 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
14 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
15 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
16 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
17 | golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
18 | golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
19 | golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
20 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
21 | golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
22 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
23 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
24 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
25 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
26 |
--------------------------------------------------------------------------------
/scripts/database/gateway/init.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE images (
2 | id SERIAL PRIMARY KEY,
3 | path character varying(255) NOT NULL
4 | );
--------------------------------------------------------------------------------
/scripts/database/history/init.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE notifications (
2 | id SERIAL PRIMARY KEY,
3 | videoId INT NOT NULL,
4 | watched TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
5 | );
--------------------------------------------------------------------------------
/scripts/stress/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.9-alpine
2 |
3 | WORKDIR /usr/src/app
4 |
5 | COPY ./ ./
6 |
7 | RUN pip install -r requirements.txt
8 |
9 | ENTRYPOINT python ./stress_test.py
--------------------------------------------------------------------------------
/scripts/stress/requirements.txt:
--------------------------------------------------------------------------------
1 | requests==2.25.1
--------------------------------------------------------------------------------
/scripts/stress/stress_test.py:
--------------------------------------------------------------------------------
1 | import requests
2 | import random
3 | import time
4 |
5 | def do_stress():
6 | while True:
7 | path = 'http://gateway:8080/images/%s' % random.randint(0, 2)
8 | try:
9 | requests.get(path)
10 | except:
11 | print("An exception occurred")
12 | time.sleep(0.5)
13 |
14 | if __name__ == "__main__":
15 | do_stress()
--------------------------------------------------------------------------------
/services/gateway/Makefile:
--------------------------------------------------------------------------------
1 | PATH_TO_PROTO = "../../api/proto/v1"
2 |
3 | build:
4 | go mod download && go build -o ./.bin/app ./cmd/main.go
5 |
6 | run: build
7 | ./.bin/app
8 |
9 | proto:
10 | protoc \
11 | --go_out=pb \
12 | --go_opt=paths=source_relative \
13 | --go-grpc_out=pb \
14 | --go-grpc_opt=paths=source_relative \
15 | --proto_path=$(PATH_TO_PROTO) $(PATH_TO_PROTO)/*.proto
16 |
17 | .DEFAULT_GOAL := run
18 | .PHONY: build, run
--------------------------------------------------------------------------------
/services/gateway/cmd/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | postgres "github.com/rvinnie/lightstream/pkg/database"
7 | "github.com/rvinnie/lightstream/services/gateway/monitoring"
8 | "github.com/rvinnie/lightstream/services/gateway/transport/amqp"
9 | "net/http"
10 | "os"
11 | "os/signal"
12 | "syscall"
13 |
14 | "github.com/rvinnie/lightstream/services/gateway/repository"
15 | "github.com/rvinnie/lightstream/services/gateway/service"
16 | "github.com/rvinnie/lightstream/services/gateway/transport/rest"
17 | "github.com/rvinnie/lightstream/services/gateway/transport/rest/handler"
18 | "google.golang.org/grpc"
19 | "google.golang.org/grpc/credentials/insecure"
20 |
21 | "github.com/joho/godotenv"
22 |
23 | "github.com/rvinnie/lightstream/services/gateway/config"
24 | "github.com/sirupsen/logrus"
25 | )
26 |
27 | const (
28 | configPath = "./config"
29 | )
30 |
31 | func main() {
32 | // Adding logger
33 | logrus.SetFormatter(new(logrus.JSONFormatter))
34 |
35 | // Initializing env variables
36 | if err := godotenv.Load(); err != nil {
37 | logrus.Fatal("Error loading .env file")
38 | }
39 |
40 | // Initializing config
41 | cfg, err := config.InitConfig(configPath)
42 | if err != nil {
43 | logrus.Fatal("Unable to parse config", err)
44 | }
45 |
46 | // Initializing postgres
47 | db, err := postgres.NewConnPool(postgres.DBConfig{
48 | Username: cfg.Postgres.Username,
49 | Password: cfg.Postgres.Password,
50 | Host: cfg.Postgres.Host,
51 | Port: cfg.Postgres.Port,
52 | DBName: cfg.Postgres.DBName,
53 | })
54 | if err != nil {
55 | logrus.Errorf("Unable to connect db: %v", err)
56 | return
57 | }
58 | defer db.Close()
59 |
60 | // Initializing RabbitMQ producer
61 | rabbitProducer, err := amqp.NewProducer(amqp.ProducerConfig{
62 | Username: cfg.RabbitMQ.Username,
63 | Password: cfg.RabbitMQ.Password,
64 | Host: cfg.RabbitMQ.Host,
65 | Port: cfg.RabbitMQ.Port,
66 | })
67 | if err != nil {
68 | logrus.Errorf("Unable to create RabbitMQ producer: %v", err)
69 | return
70 | }
71 | logrus.Info("History RabbitMQ (AMQP) producer is created")
72 |
73 | // Initializing gRPC connection
74 | grpcTarget := fmt.Sprintf("%s:%s", cfg.GRPC.Host, cfg.GRPC.Port)
75 | grpcConn, err := grpc.Dial(grpcTarget, grpc.WithTransportCredentials(insecure.NewCredentials()))
76 | if err != nil {
77 | logrus.Fatal(err)
78 | }
79 | defer grpcConn.Close()
80 | logrus.Info("Storage (gRPC) client is created")
81 |
82 | // Initializing Prometheus
83 | metrics := monitoring.CreateMetrics(cfg.Prometheus.MaxConcurrentRequests)
84 |
85 | imagesRepository := repository.NewImagesPostgres(db)
86 | imagesService := service.NewImagesService(imagesRepository)
87 | imagesHandler := handler.NewImagesHandler(grpcConn, imagesService, rabbitProducer, metrics)
88 |
89 | restServer := rest.NewServer(cfg, imagesHandler.InitRoutes(*cfg))
90 | go func() {
91 | if err = restServer.Run(); err != http.ErrServerClosed {
92 | logrus.Fatalf("error occured while running gateway (HTTP) server: %s", err.Error())
93 | }
94 | }()
95 | logrus.Info("Gateway (HTTP) server is running")
96 |
97 | // Gracefull shutdown
98 | quit := make(chan os.Signal, 1)
99 | signal.Notify(quit, os.Interrupt, syscall.SIGQUIT, syscall.SIGTERM)
100 |
101 | <-quit
102 |
103 | logrus.Info("History RabbitMQ (AMQP) producer shutting down")
104 | if err = rabbitProducer.Shutdown(); err != nil {
105 | logrus.Errorf("Error on history RabbitMQ (AMQP) producer shutting down: %s", err.Error())
106 | }
107 |
108 | logrus.Info("Gateway (HTTP) server shutting down")
109 | if err = restServer.Stop(context.Background()); err != nil {
110 | logrus.Errorf("Error on gateway (HTTP) server shutting down: %s", err.Error())
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/services/gateway/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "os"
5 | "time"
6 |
7 | "github.com/spf13/viper"
8 | )
9 |
10 | type Config struct {
11 | HTTP HTTPConfig
12 | GRPC GRPCConfig
13 | Postgres PostgresConfig
14 | RabbitMQ RabbitMQConfig
15 | Prometheus PrometheusConfig
16 | GIN GINConfig
17 | }
18 |
19 | type HTTPConfig struct {
20 | Host string `yaml:"host"`
21 | Port string `yaml:"port"`
22 | ReadTimeout time.Duration `yaml:"readTimeout"`
23 | WriteTimeout time.Duration `yaml:"writeTimeout"`
24 | }
25 |
26 | type GRPCConfig struct {
27 | Host string `yaml:"host"`
28 | Port string `yaml:"port"`
29 | }
30 |
31 | type PostgresConfig struct {
32 | Username string
33 | Password string
34 | Host string
35 | Port string
36 | DBName string
37 | }
38 |
39 | type RabbitMQConfig struct {
40 | Username string
41 | Password string
42 | Host string `yaml:"host"`
43 | Port string `yaml:"port"`
44 | }
45 |
46 | type PrometheusConfig struct {
47 | MaxConcurrentRequests float64 `yaml:"maxConcurrentRequests"`
48 | }
49 |
50 | type GINConfig struct {
51 | Mode string
52 | }
53 |
54 | func InitConfig(configDir string) (*Config, error) {
55 | viper.AddConfigPath(configDir)
56 | viper.SetConfigName("gateway")
57 | if err := viper.ReadInConfig(); err != nil {
58 | return nil, err
59 | }
60 |
61 | var cfg Config
62 | if err := viper.UnmarshalKey("http", &cfg.HTTP); err != nil {
63 | return nil, err
64 | }
65 | if err := viper.UnmarshalKey("gRPC", &cfg.GRPC); err != nil {
66 | return nil, err
67 | }
68 | if err := viper.UnmarshalKey("rabbit", &cfg.RabbitMQ); err != nil {
69 | return nil, err
70 | }
71 | if err := viper.UnmarshalKey("prometheus", &cfg.Prometheus); err != nil {
72 | return nil, err
73 | }
74 |
75 | setEnvVariables(&cfg)
76 |
77 | return &cfg, nil
78 | }
79 |
80 | func setEnvVariables(cfg *Config) {
81 | cfg.GIN.Mode = os.Getenv("GIN_MODE")
82 | cfg.Postgres.Username = os.Getenv("POSTGRES_USER")
83 | cfg.Postgres.Password = os.Getenv("POSTGRES_PASSWORD")
84 | cfg.Postgres.Host = os.Getenv("DATABASE_HOST")
85 | cfg.Postgres.DBName = os.Getenv("POSTGRES_DB")
86 |
87 | cfg.RabbitMQ.Username = os.Getenv("RABBIT_USER")
88 | cfg.RabbitMQ.Password = os.Getenv("RABBIT_PASSWORD")
89 | }
90 |
--------------------------------------------------------------------------------
/services/gateway/config/gateway.yaml:
--------------------------------------------------------------------------------
1 | http:
2 | host: 0.0.0.0
3 | port: 8080
4 | readTimeout: 10s
5 | writeTimeout: 10s
6 |
7 | gRPC:
8 | host: storage
9 | port: 4040
10 |
11 | rabbit:
12 | host: rabbit
13 | port: 5672
14 |
15 | prometheus:
16 | maxConcurrentRequests: 20.0
--------------------------------------------------------------------------------
/services/gateway/dev.Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.19-alpine
2 |
3 | WORKDIR /usr/src/app
4 |
5 | ENV CGO_ENABLED=0
6 |
7 | COPY ./ ./
8 |
9 | RUN apk add --no-cache make \
10 | && go mod download \
11 | && go get github.com/githubnemo/CompileDaemon \
12 | && go install github.com/githubnemo/CompileDaemon
13 |
14 | ENTRYPOINT CompileDaemon -build="go build -o ./.bin/app ./cmd/main.go" -command="./.bin/app"
--------------------------------------------------------------------------------
/services/gateway/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/rvinnie/lightstream/services/gateway
2 |
3 | go 1.19
4 |
5 | require (
6 | github.com/gin-gonic/gin v1.9.0
7 | github.com/joho/godotenv v1.5.1
8 | github.com/sirupsen/logrus v1.9.2
9 | github.com/spf13/viper v1.15.0
10 | google.golang.org/grpc v1.55.0
11 | )
12 |
13 | require (
14 | github.com/beorn7/perks v1.0.1 // indirect
15 | github.com/bytedance/sonic v1.8.0 // indirect
16 | github.com/cespare/xxhash/v2 v2.2.0 // indirect
17 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
18 | github.com/fsnotify/fsnotify v1.6.0 // indirect
19 | github.com/gin-contrib/cors v1.4.0 // indirect
20 | github.com/gin-contrib/sse v0.1.0 // indirect
21 | github.com/go-playground/locales v0.14.1 // indirect
22 | github.com/go-playground/universal-translator v0.18.1 // indirect
23 | github.com/go-playground/validator/v10 v10.11.2 // indirect
24 | github.com/goccy/go-json v0.10.0 // indirect
25 | github.com/golang/protobuf v1.5.3 // indirect
26 | github.com/golang/snappy v0.0.1 // indirect
27 | github.com/hashicorp/hcl v1.0.0 // indirect
28 | github.com/jackc/pgpassfile v1.0.0 // indirect
29 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
30 | github.com/jackc/pgx/v5 v5.3.1 // indirect
31 | github.com/jackc/puddle/v2 v2.2.0 // indirect
32 | github.com/json-iterator/go v1.1.12 // indirect
33 | github.com/klauspost/compress v1.13.6 // indirect
34 | github.com/klauspost/cpuid/v2 v2.0.9 // indirect
35 | github.com/leodido/go-urn v1.2.1 // indirect
36 | github.com/magiconair/properties v1.8.7 // indirect
37 | github.com/mattn/go-isatty v0.0.17 // indirect
38 | github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
39 | github.com/mitchellh/mapstructure v1.5.0 // indirect
40 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
41 | github.com/modern-go/reflect2 v1.0.2 // indirect
42 | github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect
43 | github.com/pelletier/go-toml/v2 v2.0.6 // indirect
44 | github.com/pkg/errors v0.9.1 // indirect
45 | github.com/prometheus/client_golang v1.15.1 // indirect
46 | github.com/prometheus/client_model v0.3.0 // indirect
47 | github.com/prometheus/common v0.42.0 // indirect
48 | github.com/prometheus/procfs v0.9.0 // indirect
49 | github.com/rabbitmq/amqp091-go v1.8.1 // indirect
50 | github.com/rvinnie/lightstream/pkg v0.0.0-20230531140318-b669ba62628a // indirect
51 | github.com/spf13/afero v1.9.3 // indirect
52 | github.com/spf13/cast v1.5.0 // indirect
53 | github.com/spf13/jwalterweatherman v1.1.0 // indirect
54 | github.com/spf13/pflag v1.0.5 // indirect
55 | github.com/subosito/gotenv v1.4.2 // indirect
56 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
57 | github.com/ugorji/go/codec v1.2.9 // indirect
58 | github.com/xdg-go/pbkdf2 v1.0.0 // indirect
59 | github.com/xdg-go/scram v1.1.1 // indirect
60 | github.com/xdg-go/stringprep v1.0.3 // indirect
61 | github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
62 | go.mongodb.org/mongo-driver v1.11.6 // indirect
63 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
64 | golang.org/x/crypto v0.6.0 // indirect
65 | golang.org/x/net v0.8.0 // indirect
66 | golang.org/x/sync v0.1.0 // indirect
67 | golang.org/x/sys v0.6.0 // indirect
68 | golang.org/x/text v0.8.0 // indirect
69 | google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 // indirect
70 | google.golang.org/protobuf v1.30.0 // indirect
71 | gopkg.in/ini.v1 v1.67.0 // indirect
72 | gopkg.in/yaml.v3 v3.0.1 // indirect
73 | )
74 |
--------------------------------------------------------------------------------
/services/gateway/monitoring/prometheus.go:
--------------------------------------------------------------------------------
1 | package monitoring
2 |
3 | import (
4 | "github.com/prometheus/client_golang/prometheus"
5 | "strconv"
6 | )
7 |
8 | const (
9 | defaultHttpConcurrentRequestsMax = 20
10 | )
11 |
12 | type Metrics struct {
13 | RequestsTotal *prometheus.CounterVec
14 | RequestsDuration *prometheus.HistogramVec
15 | ConcurrentRequests prometheus.Gauge
16 | ConcurrentRequestsMax prometheus.Gauge
17 | }
18 |
19 | func CreateMetrics(httpConcurrentRequestsMax float64) *Metrics {
20 | if httpConcurrentRequestsMax == 0 {
21 | httpConcurrentRequestsMax = defaultHttpConcurrentRequestsMax
22 | }
23 |
24 | metrics := Metrics{}
25 |
26 | metrics.RequestsTotal = prometheus.NewCounterVec(
27 | prometheus.CounterOpts{
28 | Name: "gateway_requests_total",
29 | Help: "The number of all requests to the service",
30 | },
31 | []string{"method", "url", "code"},
32 | )
33 |
34 | metrics.RequestsDuration = prometheus.NewHistogramVec(
35 | prometheus.HistogramOpts{
36 | Name: "gateway_requests_duration",
37 | Help: "Request processing time",
38 | Buckets: prometheus.LinearBuckets(0.020, 0.020, 5),
39 | },
40 | []string{"method", "url", "code"},
41 | )
42 |
43 | metrics.ConcurrentRequests = prometheus.NewGauge(
44 | prometheus.GaugeOpts{
45 | Name: "http_concurrent_requests",
46 | Help: "The number of inflight requests",
47 | },
48 | )
49 |
50 | metrics.ConcurrentRequestsMax = prometheus.NewGauge(
51 | prometheus.GaugeOpts{
52 | Name: "http_concurrent_requests_max",
53 | },
54 | )
55 |
56 | metrics.ConcurrentRequestsMax.Set(httpConcurrentRequestsMax)
57 |
58 | prometheus.MustRegister(metrics.RequestsTotal)
59 | prometheus.MustRegister(metrics.RequestsDuration)
60 | prometheus.MustRegister(metrics.ConcurrentRequests)
61 | prometheus.MustRegister(metrics.ConcurrentRequestsMax)
62 |
63 | return &metrics
64 | }
65 |
66 | func (m *Metrics) CollectMetrics(method string, url string, statusCode int, duration float64) {
67 | m.RequestsTotal.With(
68 | prometheus.Labels{
69 | "method": method,
70 | "url": url,
71 | "code": strconv.Itoa(statusCode),
72 | }).Inc()
73 |
74 | m.RequestsDuration.With(
75 | prometheus.Labels{
76 | "method": method,
77 | "url": url,
78 | "code": strconv.Itoa(statusCode),
79 | }).Observe(duration)
80 | }
81 |
--------------------------------------------------------------------------------
/services/gateway/pb/image_storage.pb.go:
--------------------------------------------------------------------------------
1 | // Code generated by protoc-gen-go. DO NOT EDIT.
2 | // versions:
3 | // protoc-gen-go v1.30.0
4 | // protoc v4.23.2
5 | // source: image_storage.proto
6 |
7 | package v1
8 |
9 | import (
10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect"
11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl"
12 | emptypb "google.golang.org/protobuf/types/known/emptypb"
13 | reflect "reflect"
14 | sync "sync"
15 | )
16 |
17 | const (
18 | // Verify that this generated code is sufficiently up-to-date.
19 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
20 | // Verify that runtime/protoimpl is sufficiently up-to-date.
21 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
22 | )
23 |
24 | type CreateImageRequest struct {
25 | state protoimpl.MessageState
26 | sizeCache protoimpl.SizeCache
27 | unknownFields protoimpl.UnknownFields
28 |
29 | Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"`
30 | ContentType string `protobuf:"bytes,2,opt,name=contentType,proto3" json:"contentType,omitempty"`
31 | Image []byte `protobuf:"bytes,3,opt,name=image,proto3" json:"image,omitempty"`
32 | }
33 |
34 | func (x *CreateImageRequest) Reset() {
35 | *x = CreateImageRequest{}
36 | if protoimpl.UnsafeEnabled {
37 | mi := &file_image_storage_proto_msgTypes[0]
38 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
39 | ms.StoreMessageInfo(mi)
40 | }
41 | }
42 |
43 | func (x *CreateImageRequest) String() string {
44 | return protoimpl.X.MessageStringOf(x)
45 | }
46 |
47 | func (*CreateImageRequest) ProtoMessage() {}
48 |
49 | func (x *CreateImageRequest) ProtoReflect() protoreflect.Message {
50 | mi := &file_image_storage_proto_msgTypes[0]
51 | if protoimpl.UnsafeEnabled && x != nil {
52 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
53 | if ms.LoadMessageInfo() == nil {
54 | ms.StoreMessageInfo(mi)
55 | }
56 | return ms
57 | }
58 | return mi.MessageOf(x)
59 | }
60 |
61 | // Deprecated: Use CreateImageRequest.ProtoReflect.Descriptor instead.
62 | func (*CreateImageRequest) Descriptor() ([]byte, []int) {
63 | return file_image_storage_proto_rawDescGZIP(), []int{0}
64 | }
65 |
66 | func (x *CreateImageRequest) GetPath() string {
67 | if x != nil {
68 | return x.Path
69 | }
70 | return ""
71 | }
72 |
73 | func (x *CreateImageRequest) GetContentType() string {
74 | if x != nil {
75 | return x.ContentType
76 | }
77 | return ""
78 | }
79 |
80 | func (x *CreateImageRequest) GetImage() []byte {
81 | if x != nil {
82 | return x.Image
83 | }
84 | return nil
85 | }
86 |
87 | type FindImageRequest struct {
88 | state protoimpl.MessageState
89 | sizeCache protoimpl.SizeCache
90 | unknownFields protoimpl.UnknownFields
91 |
92 | Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"`
93 | }
94 |
95 | func (x *FindImageRequest) Reset() {
96 | *x = FindImageRequest{}
97 | if protoimpl.UnsafeEnabled {
98 | mi := &file_image_storage_proto_msgTypes[1]
99 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
100 | ms.StoreMessageInfo(mi)
101 | }
102 | }
103 |
104 | func (x *FindImageRequest) String() string {
105 | return protoimpl.X.MessageStringOf(x)
106 | }
107 |
108 | func (*FindImageRequest) ProtoMessage() {}
109 |
110 | func (x *FindImageRequest) ProtoReflect() protoreflect.Message {
111 | mi := &file_image_storage_proto_msgTypes[1]
112 | if protoimpl.UnsafeEnabled && x != nil {
113 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
114 | if ms.LoadMessageInfo() == nil {
115 | ms.StoreMessageInfo(mi)
116 | }
117 | return ms
118 | }
119 | return mi.MessageOf(x)
120 | }
121 |
122 | // Deprecated: Use FindImageRequest.ProtoReflect.Descriptor instead.
123 | func (*FindImageRequest) Descriptor() ([]byte, []int) {
124 | return file_image_storage_proto_rawDescGZIP(), []int{1}
125 | }
126 |
127 | func (x *FindImageRequest) GetPath() string {
128 | if x != nil {
129 | return x.Path
130 | }
131 | return ""
132 | }
133 |
134 | type FindImagesRequest struct {
135 | state protoimpl.MessageState
136 | sizeCache protoimpl.SizeCache
137 | unknownFields protoimpl.UnknownFields
138 |
139 | Paths []string `protobuf:"bytes,1,rep,name=paths,proto3" json:"paths,omitempty"`
140 | }
141 |
142 | func (x *FindImagesRequest) Reset() {
143 | *x = FindImagesRequest{}
144 | if protoimpl.UnsafeEnabled {
145 | mi := &file_image_storage_proto_msgTypes[2]
146 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
147 | ms.StoreMessageInfo(mi)
148 | }
149 | }
150 |
151 | func (x *FindImagesRequest) String() string {
152 | return protoimpl.X.MessageStringOf(x)
153 | }
154 |
155 | func (*FindImagesRequest) ProtoMessage() {}
156 |
157 | func (x *FindImagesRequest) ProtoReflect() protoreflect.Message {
158 | mi := &file_image_storage_proto_msgTypes[2]
159 | if protoimpl.UnsafeEnabled && x != nil {
160 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
161 | if ms.LoadMessageInfo() == nil {
162 | ms.StoreMessageInfo(mi)
163 | }
164 | return ms
165 | }
166 | return mi.MessageOf(x)
167 | }
168 |
169 | // Deprecated: Use FindImagesRequest.ProtoReflect.Descriptor instead.
170 | func (*FindImagesRequest) Descriptor() ([]byte, []int) {
171 | return file_image_storage_proto_rawDescGZIP(), []int{2}
172 | }
173 |
174 | func (x *FindImagesRequest) GetPaths() []string {
175 | if x != nil {
176 | return x.Paths
177 | }
178 | return nil
179 | }
180 |
181 | type FindImageResponse struct {
182 | state protoimpl.MessageState
183 | sizeCache protoimpl.SizeCache
184 | unknownFields protoimpl.UnknownFields
185 |
186 | Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
187 | ContentType string `protobuf:"bytes,2,opt,name=contentType,proto3" json:"contentType,omitempty"`
188 | Image []byte `protobuf:"bytes,3,opt,name=image,proto3" json:"image,omitempty"`
189 | }
190 |
191 | func (x *FindImageResponse) Reset() {
192 | *x = FindImageResponse{}
193 | if protoimpl.UnsafeEnabled {
194 | mi := &file_image_storage_proto_msgTypes[3]
195 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
196 | ms.StoreMessageInfo(mi)
197 | }
198 | }
199 |
200 | func (x *FindImageResponse) String() string {
201 | return protoimpl.X.MessageStringOf(x)
202 | }
203 |
204 | func (*FindImageResponse) ProtoMessage() {}
205 |
206 | func (x *FindImageResponse) ProtoReflect() protoreflect.Message {
207 | mi := &file_image_storage_proto_msgTypes[3]
208 | if protoimpl.UnsafeEnabled && x != nil {
209 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
210 | if ms.LoadMessageInfo() == nil {
211 | ms.StoreMessageInfo(mi)
212 | }
213 | return ms
214 | }
215 | return mi.MessageOf(x)
216 | }
217 |
218 | // Deprecated: Use FindImageResponse.ProtoReflect.Descriptor instead.
219 | func (*FindImageResponse) Descriptor() ([]byte, []int) {
220 | return file_image_storage_proto_rawDescGZIP(), []int{3}
221 | }
222 |
223 | func (x *FindImageResponse) GetName() string {
224 | if x != nil {
225 | return x.Name
226 | }
227 | return ""
228 | }
229 |
230 | func (x *FindImageResponse) GetContentType() string {
231 | if x != nil {
232 | return x.ContentType
233 | }
234 | return ""
235 | }
236 |
237 | func (x *FindImageResponse) GetImage() []byte {
238 | if x != nil {
239 | return x.Image
240 | }
241 | return nil
242 | }
243 |
244 | type FindImagesResponse struct {
245 | state protoimpl.MessageState
246 | sizeCache protoimpl.SizeCache
247 | unknownFields protoimpl.UnknownFields
248 |
249 | Images []*FindImageResponse `protobuf:"bytes,1,rep,name=images,proto3" json:"images,omitempty"`
250 | }
251 |
252 | func (x *FindImagesResponse) Reset() {
253 | *x = FindImagesResponse{}
254 | if protoimpl.UnsafeEnabled {
255 | mi := &file_image_storage_proto_msgTypes[4]
256 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
257 | ms.StoreMessageInfo(mi)
258 | }
259 | }
260 |
261 | func (x *FindImagesResponse) String() string {
262 | return protoimpl.X.MessageStringOf(x)
263 | }
264 |
265 | func (*FindImagesResponse) ProtoMessage() {}
266 |
267 | func (x *FindImagesResponse) ProtoReflect() protoreflect.Message {
268 | mi := &file_image_storage_proto_msgTypes[4]
269 | if protoimpl.UnsafeEnabled && x != nil {
270 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
271 | if ms.LoadMessageInfo() == nil {
272 | ms.StoreMessageInfo(mi)
273 | }
274 | return ms
275 | }
276 | return mi.MessageOf(x)
277 | }
278 |
279 | // Deprecated: Use FindImagesResponse.ProtoReflect.Descriptor instead.
280 | func (*FindImagesResponse) Descriptor() ([]byte, []int) {
281 | return file_image_storage_proto_rawDescGZIP(), []int{4}
282 | }
283 |
284 | func (x *FindImagesResponse) GetImages() []*FindImageResponse {
285 | if x != nil {
286 | return x.Images
287 | }
288 | return nil
289 | }
290 |
291 | var File_image_storage_proto protoreflect.FileDescriptor
292 |
293 | var file_image_storage_proto_rawDesc = []byte{
294 | 0x0a, 0x13, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e,
295 | 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x07, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x1a, 0x1b,
296 | 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f,
297 | 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x60, 0x0a, 0x12, 0x43,
298 | 0x72, 0x65, 0x61, 0x74, 0x65, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
299 | 0x74, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
300 | 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x20, 0x0a, 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74,
301 | 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x74,
302 | 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65,
303 | 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x22, 0x26, 0x0a,
304 | 0x10, 0x46, 0x69, 0x6e, 0x64, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
305 | 0x74, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
306 | 0x04, 0x70, 0x61, 0x74, 0x68, 0x22, 0x29, 0x0a, 0x11, 0x46, 0x69, 0x6e, 0x64, 0x49, 0x6d, 0x61,
307 | 0x67, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x70, 0x61,
308 | 0x74, 0x68, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x70, 0x61, 0x74, 0x68, 0x73,
309 | 0x22, 0x5f, 0x0a, 0x11, 0x46, 0x69, 0x6e, 0x64, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73,
310 | 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20,
311 | 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x63, 0x6f, 0x6e,
312 | 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b,
313 | 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x69,
314 | 0x6d, 0x61, 0x67, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x69, 0x6d, 0x61, 0x67,
315 | 0x65, 0x22, 0x48, 0x0a, 0x12, 0x46, 0x69, 0x6e, 0x64, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x73, 0x52,
316 | 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x32, 0x0a, 0x06, 0x69, 0x6d, 0x61, 0x67, 0x65,
317 | 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61,
318 | 0x79, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f,
319 | 0x6e, 0x73, 0x65, 0x52, 0x06, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x73, 0x32, 0xe1, 0x01, 0x0a, 0x0c,
320 | 0x49, 0x6d, 0x61, 0x67, 0x65, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x12, 0x44, 0x0a, 0x0b,
321 | 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x1b, 0x2e, 0x67, 0x61,
322 | 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x49, 0x6d, 0x61, 0x67,
323 | 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
324 | 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79,
325 | 0x22, 0x00, 0x12, 0x43, 0x0a, 0x08, 0x47, 0x65, 0x74, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x19,
326 | 0x2e, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x49, 0x6d, 0x61,
327 | 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x67, 0x61, 0x74, 0x65,
328 | 0x77, 0x61, 0x79, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73,
329 | 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x46, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x49, 0x6d,
330 | 0x61, 0x67, 0x65, 0x73, 0x12, 0x1a, 0x2e, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x46,
331 | 0x69, 0x6e, 0x64, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
332 | 0x1a, 0x1b, 0x2e, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x49,
333 | 0x6d, 0x61, 0x67, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42,
334 | 0x2d, 0x5a, 0x2b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x72, 0x76,
335 | 0x69, 0x6e, 0x6e, 0x69, 0x65, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x73, 0x74, 0x72, 0x65, 0x61,
336 | 0x6d, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06,
337 | 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
338 | }
339 |
340 | var (
341 | file_image_storage_proto_rawDescOnce sync.Once
342 | file_image_storage_proto_rawDescData = file_image_storage_proto_rawDesc
343 | )
344 |
345 | func file_image_storage_proto_rawDescGZIP() []byte {
346 | file_image_storage_proto_rawDescOnce.Do(func() {
347 | file_image_storage_proto_rawDescData = protoimpl.X.CompressGZIP(file_image_storage_proto_rawDescData)
348 | })
349 | return file_image_storage_proto_rawDescData
350 | }
351 |
352 | var file_image_storage_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
353 | var file_image_storage_proto_goTypes = []interface{}{
354 | (*CreateImageRequest)(nil), // 0: gateway.CreateImageRequest
355 | (*FindImageRequest)(nil), // 1: gateway.FindImageRequest
356 | (*FindImagesRequest)(nil), // 2: gateway.FindImagesRequest
357 | (*FindImageResponse)(nil), // 3: gateway.FindImageResponse
358 | (*FindImagesResponse)(nil), // 4: gateway.FindImagesResponse
359 | (*emptypb.Empty)(nil), // 5: google.protobuf.Empty
360 | }
361 | var file_image_storage_proto_depIdxs = []int32{
362 | 3, // 0: gateway.FindImagesResponse.images:type_name -> gateway.FindImageResponse
363 | 0, // 1: gateway.ImageStorage.CreateImage:input_type -> gateway.CreateImageRequest
364 | 1, // 2: gateway.ImageStorage.GetImage:input_type -> gateway.FindImageRequest
365 | 2, // 3: gateway.ImageStorage.GetImages:input_type -> gateway.FindImagesRequest
366 | 5, // 4: gateway.ImageStorage.CreateImage:output_type -> google.protobuf.Empty
367 | 3, // 5: gateway.ImageStorage.GetImage:output_type -> gateway.FindImageResponse
368 | 4, // 6: gateway.ImageStorage.GetImages:output_type -> gateway.FindImagesResponse
369 | 4, // [4:7] is the sub-list for method output_type
370 | 1, // [1:4] is the sub-list for method input_type
371 | 1, // [1:1] is the sub-list for extension type_name
372 | 1, // [1:1] is the sub-list for extension extendee
373 | 0, // [0:1] is the sub-list for field type_name
374 | }
375 |
376 | func init() { file_image_storage_proto_init() }
377 | func file_image_storage_proto_init() {
378 | if File_image_storage_proto != nil {
379 | return
380 | }
381 | if !protoimpl.UnsafeEnabled {
382 | file_image_storage_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
383 | switch v := v.(*CreateImageRequest); i {
384 | case 0:
385 | return &v.state
386 | case 1:
387 | return &v.sizeCache
388 | case 2:
389 | return &v.unknownFields
390 | default:
391 | return nil
392 | }
393 | }
394 | file_image_storage_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
395 | switch v := v.(*FindImageRequest); i {
396 | case 0:
397 | return &v.state
398 | case 1:
399 | return &v.sizeCache
400 | case 2:
401 | return &v.unknownFields
402 | default:
403 | return nil
404 | }
405 | }
406 | file_image_storage_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
407 | switch v := v.(*FindImagesRequest); i {
408 | case 0:
409 | return &v.state
410 | case 1:
411 | return &v.sizeCache
412 | case 2:
413 | return &v.unknownFields
414 | default:
415 | return nil
416 | }
417 | }
418 | file_image_storage_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
419 | switch v := v.(*FindImageResponse); i {
420 | case 0:
421 | return &v.state
422 | case 1:
423 | return &v.sizeCache
424 | case 2:
425 | return &v.unknownFields
426 | default:
427 | return nil
428 | }
429 | }
430 | file_image_storage_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
431 | switch v := v.(*FindImagesResponse); i {
432 | case 0:
433 | return &v.state
434 | case 1:
435 | return &v.sizeCache
436 | case 2:
437 | return &v.unknownFields
438 | default:
439 | return nil
440 | }
441 | }
442 | }
443 | type x struct{}
444 | out := protoimpl.TypeBuilder{
445 | File: protoimpl.DescBuilder{
446 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
447 | RawDescriptor: file_image_storage_proto_rawDesc,
448 | NumEnums: 0,
449 | NumMessages: 5,
450 | NumExtensions: 0,
451 | NumServices: 1,
452 | },
453 | GoTypes: file_image_storage_proto_goTypes,
454 | DependencyIndexes: file_image_storage_proto_depIdxs,
455 | MessageInfos: file_image_storage_proto_msgTypes,
456 | }.Build()
457 | File_image_storage_proto = out.File
458 | file_image_storage_proto_rawDesc = nil
459 | file_image_storage_proto_goTypes = nil
460 | file_image_storage_proto_depIdxs = nil
461 | }
462 |
--------------------------------------------------------------------------------
/services/gateway/pb/image_storage_grpc.pb.go:
--------------------------------------------------------------------------------
1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
2 | // versions:
3 | // - protoc-gen-go-grpc v1.3.0
4 | // - protoc v4.23.2
5 | // source: image_storage.proto
6 |
7 | package v1
8 |
9 | import (
10 | context "context"
11 | grpc "google.golang.org/grpc"
12 | codes "google.golang.org/grpc/codes"
13 | status "google.golang.org/grpc/status"
14 | emptypb "google.golang.org/protobuf/types/known/emptypb"
15 | )
16 |
17 | // This is a compile-time assertion to ensure that this generated file
18 | // is compatible with the grpc package it is being compiled against.
19 | // Requires gRPC-Go v1.32.0 or later.
20 | const _ = grpc.SupportPackageIsVersion7
21 |
22 | const (
23 | ImageStorage_CreateImage_FullMethodName = "/gateway.ImageStorage/CreateImage"
24 | ImageStorage_GetImage_FullMethodName = "/gateway.ImageStorage/GetImage"
25 | ImageStorage_GetImages_FullMethodName = "/gateway.ImageStorage/GetImages"
26 | )
27 |
28 | // ImageStorageClient is the client API for ImageStorage service.
29 | //
30 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
31 | type ImageStorageClient interface {
32 | CreateImage(ctx context.Context, in *CreateImageRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
33 | GetImage(ctx context.Context, in *FindImageRequest, opts ...grpc.CallOption) (*FindImageResponse, error)
34 | GetImages(ctx context.Context, in *FindImagesRequest, opts ...grpc.CallOption) (*FindImagesResponse, error)
35 | }
36 |
37 | type imageStorageClient struct {
38 | cc grpc.ClientConnInterface
39 | }
40 |
41 | func NewImageStorageClient(cc grpc.ClientConnInterface) ImageStorageClient {
42 | return &imageStorageClient{cc}
43 | }
44 |
45 | func (c *imageStorageClient) CreateImage(ctx context.Context, in *CreateImageRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
46 | out := new(emptypb.Empty)
47 | err := c.cc.Invoke(ctx, ImageStorage_CreateImage_FullMethodName, in, out, opts...)
48 | if err != nil {
49 | return nil, err
50 | }
51 | return out, nil
52 | }
53 |
54 | func (c *imageStorageClient) GetImage(ctx context.Context, in *FindImageRequest, opts ...grpc.CallOption) (*FindImageResponse, error) {
55 | out := new(FindImageResponse)
56 | err := c.cc.Invoke(ctx, ImageStorage_GetImage_FullMethodName, in, out, opts...)
57 | if err != nil {
58 | return nil, err
59 | }
60 | return out, nil
61 | }
62 |
63 | func (c *imageStorageClient) GetImages(ctx context.Context, in *FindImagesRequest, opts ...grpc.CallOption) (*FindImagesResponse, error) {
64 | out := new(FindImagesResponse)
65 | err := c.cc.Invoke(ctx, ImageStorage_GetImages_FullMethodName, in, out, opts...)
66 | if err != nil {
67 | return nil, err
68 | }
69 | return out, nil
70 | }
71 |
72 | // ImageStorageServer is the server API for ImageStorage service.
73 | // All implementations must embed UnimplementedImageStorageServer
74 | // for forward compatibility
75 | type ImageStorageServer interface {
76 | CreateImage(context.Context, *CreateImageRequest) (*emptypb.Empty, error)
77 | GetImage(context.Context, *FindImageRequest) (*FindImageResponse, error)
78 | GetImages(context.Context, *FindImagesRequest) (*FindImagesResponse, error)
79 | mustEmbedUnimplementedImageStorageServer()
80 | }
81 |
82 | // UnimplementedImageStorageServer must be embedded to have forward compatible implementations.
83 | type UnimplementedImageStorageServer struct {
84 | }
85 |
86 | func (UnimplementedImageStorageServer) CreateImage(context.Context, *CreateImageRequest) (*emptypb.Empty, error) {
87 | return nil, status.Errorf(codes.Unimplemented, "method CreateImage not implemented")
88 | }
89 | func (UnimplementedImageStorageServer) GetImage(context.Context, *FindImageRequest) (*FindImageResponse, error) {
90 | return nil, status.Errorf(codes.Unimplemented, "method GetImage not implemented")
91 | }
92 | func (UnimplementedImageStorageServer) GetImages(context.Context, *FindImagesRequest) (*FindImagesResponse, error) {
93 | return nil, status.Errorf(codes.Unimplemented, "method GetImages not implemented")
94 | }
95 | func (UnimplementedImageStorageServer) mustEmbedUnimplementedImageStorageServer() {}
96 |
97 | // UnsafeImageStorageServer may be embedded to opt out of forward compatibility for this service.
98 | // Use of this interface is not recommended, as added methods to ImageStorageServer will
99 | // result in compilation errors.
100 | type UnsafeImageStorageServer interface {
101 | mustEmbedUnimplementedImageStorageServer()
102 | }
103 |
104 | func RegisterImageStorageServer(s grpc.ServiceRegistrar, srv ImageStorageServer) {
105 | s.RegisterService(&ImageStorage_ServiceDesc, srv)
106 | }
107 |
108 | func _ImageStorage_CreateImage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
109 | in := new(CreateImageRequest)
110 | if err := dec(in); err != nil {
111 | return nil, err
112 | }
113 | if interceptor == nil {
114 | return srv.(ImageStorageServer).CreateImage(ctx, in)
115 | }
116 | info := &grpc.UnaryServerInfo{
117 | Server: srv,
118 | FullMethod: ImageStorage_CreateImage_FullMethodName,
119 | }
120 | handler := func(ctx context.Context, req interface{}) (interface{}, error) {
121 | return srv.(ImageStorageServer).CreateImage(ctx, req.(*CreateImageRequest))
122 | }
123 | return interceptor(ctx, in, info, handler)
124 | }
125 |
126 | func _ImageStorage_GetImage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
127 | in := new(FindImageRequest)
128 | if err := dec(in); err != nil {
129 | return nil, err
130 | }
131 | if interceptor == nil {
132 | return srv.(ImageStorageServer).GetImage(ctx, in)
133 | }
134 | info := &grpc.UnaryServerInfo{
135 | Server: srv,
136 | FullMethod: ImageStorage_GetImage_FullMethodName,
137 | }
138 | handler := func(ctx context.Context, req interface{}) (interface{}, error) {
139 | return srv.(ImageStorageServer).GetImage(ctx, req.(*FindImageRequest))
140 | }
141 | return interceptor(ctx, in, info, handler)
142 | }
143 |
144 | func _ImageStorage_GetImages_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
145 | in := new(FindImagesRequest)
146 | if err := dec(in); err != nil {
147 | return nil, err
148 | }
149 | if interceptor == nil {
150 | return srv.(ImageStorageServer).GetImages(ctx, in)
151 | }
152 | info := &grpc.UnaryServerInfo{
153 | Server: srv,
154 | FullMethod: ImageStorage_GetImages_FullMethodName,
155 | }
156 | handler := func(ctx context.Context, req interface{}) (interface{}, error) {
157 | return srv.(ImageStorageServer).GetImages(ctx, req.(*FindImagesRequest))
158 | }
159 | return interceptor(ctx, in, info, handler)
160 | }
161 |
162 | // ImageStorage_ServiceDesc is the grpc.ServiceDesc for ImageStorage service.
163 | // It's only intended for direct use with grpc.RegisterService,
164 | // and not to be introspected or modified (even as a copy)
165 | var ImageStorage_ServiceDesc = grpc.ServiceDesc{
166 | ServiceName: "gateway.ImageStorage",
167 | HandlerType: (*ImageStorageServer)(nil),
168 | Methods: []grpc.MethodDesc{
169 | {
170 | MethodName: "CreateImage",
171 | Handler: _ImageStorage_CreateImage_Handler,
172 | },
173 | {
174 | MethodName: "GetImage",
175 | Handler: _ImageStorage_GetImage_Handler,
176 | },
177 | {
178 | MethodName: "GetImages",
179 | Handler: _ImageStorage_GetImages_Handler,
180 | },
181 | },
182 | Streams: []grpc.StreamDesc{},
183 | Metadata: "image_storage.proto",
184 | }
185 |
--------------------------------------------------------------------------------
/services/gateway/prod.Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.19-alpine
2 |
3 | WORKDIR /usr/src/app
4 |
5 | ENV CGO_ENABLED=0
6 |
7 | COPY ./ ./
8 |
9 | RUN apk add --no-cache make && go mod download
10 |
11 | ENTRYPOINT go build -o ./.bin/app ./cmd/main.go && ./.bin/app
--------------------------------------------------------------------------------
/services/gateway/repository/images_postgres.go:
--------------------------------------------------------------------------------
1 | package repository
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "github.com/jackc/pgx/v5/pgxpool"
7 | )
8 |
9 | const (
10 | imagesCollection = "images"
11 | )
12 |
13 | type Images interface {
14 | Create(ctx context.Context, path string) (int, error)
15 | GetAll(ctx context.Context) ([]string, error)
16 | GetById(ctx context.Context, id int) (string, error)
17 | }
18 |
19 | type ImagesPostgres struct {
20 | db *pgxpool.Pool
21 | }
22 |
23 | func NewImagesPostgres(db *pgxpool.Pool) *ImagesPostgres {
24 | return &ImagesPostgres{db: db}
25 | }
26 |
27 | func (r *ImagesPostgres) Create(ctx context.Context, path string) (int, error) {
28 | query := fmt.Sprintf("INSERT INTO %s (path) VALUES ($1) RETURNING id", imagesCollection)
29 | row := r.db.QueryRow(ctx, query, path)
30 |
31 | var id int
32 | if err := row.Scan(&id); err != nil {
33 | return 0, err
34 | }
35 |
36 | return id, nil
37 | }
38 |
39 | func (r *ImagesPostgres) GetAll(ctx context.Context) ([]string, error) {
40 | query := fmt.Sprintf("SELECT path FROM %s", imagesCollection)
41 | rows, err := r.db.Query(ctx, query)
42 | defer rows.Close()
43 |
44 | var paths []string
45 | for rows.Next() {
46 | var path string
47 | err = rows.Scan(&path)
48 | if err != nil {
49 | return nil, err
50 | }
51 | paths = append(paths, path)
52 | }
53 |
54 | return paths, nil
55 | }
56 |
57 | func (r *ImagesPostgres) GetById(ctx context.Context, id int) (string, error) {
58 | query := fmt.Sprintf("SELECT path FROM %s WHERE id = $1", imagesCollection)
59 | row := r.db.QueryRow(ctx, query, id)
60 |
61 | var path string
62 | err := row.Scan(&path)
63 |
64 | return path, err
65 | }
66 |
--------------------------------------------------------------------------------
/services/gateway/service/images.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/rvinnie/lightstream/services/gateway/repository"
7 | )
8 |
9 | type Images interface {
10 | Create(ctx context.Context, name string) (int, error)
11 | GetAll(ctx context.Context) ([]string, error)
12 | GetById(ctx context.Context, id int) (string, error)
13 | }
14 |
15 | type ImagesService struct {
16 | repo repository.Images
17 | }
18 |
19 | func NewImagesService(repo repository.Images) *ImagesService {
20 | return &ImagesService{repo: repo}
21 | }
22 |
23 | func (s *ImagesService) Create(ctx context.Context, path string) (int, error) {
24 | return s.repo.Create(ctx, path)
25 | }
26 |
27 | func (s *ImagesService) GetAll(ctx context.Context) ([]string, error) {
28 | return s.repo.GetAll(ctx)
29 | }
30 |
31 | func (s *ImagesService) GetById(ctx context.Context, id int) (string, error) {
32 | return s.repo.GetById(ctx, id)
33 | }
34 |
--------------------------------------------------------------------------------
/services/gateway/transport/amqp/producer.go:
--------------------------------------------------------------------------------
1 | package amqp
2 |
3 | import (
4 | "context"
5 | "github.com/rabbitmq/amqp091-go"
6 | "net"
7 | "net/url"
8 | "strconv"
9 | "time"
10 | )
11 |
12 | const (
13 | exchangeName = "history"
14 | exchangeKind = "fanout"
15 | )
16 |
17 | type ProducerConfig struct {
18 | Username string
19 | Password string
20 | Host string
21 | Port string
22 | }
23 |
24 | type Producer struct {
25 | conn *amqp091.Connection
26 | ch *amqp091.Channel
27 | }
28 |
29 | func NewProducer(config ProducerConfig) (*Producer, error) {
30 | var err error
31 | p := &Producer{}
32 | url := formUrl("amqp", config.Username, config.Password, config.Host, config.Port)
33 |
34 | p.conn, err = amqp091.Dial(url)
35 | if err != nil {
36 | return p, err
37 | }
38 |
39 | p.ch, err = p.conn.Channel()
40 | if err != nil {
41 | return p, err
42 | }
43 |
44 | err = p.ch.ExchangeDeclare(exchangeName, exchangeKind, true, false, false, false, nil)
45 |
46 | return p, err
47 | }
48 |
49 | func (p *Producer) Publish(imageId int) error {
50 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
51 | defer cancel()
52 |
53 | err := p.ch.PublishWithContext(ctx,
54 | exchangeName,
55 | "",
56 | false,
57 | false,
58 | amqp091.Publishing{
59 | ContentType: "text/plain",
60 | Body: []byte(strconv.Itoa(imageId)),
61 | },
62 | )
63 |
64 | return err
65 | }
66 |
67 | func (p *Producer) Shutdown() error {
68 | if err := p.ch.Close(); err != nil {
69 | return err
70 | }
71 |
72 | if err := p.conn.Close(); err != nil {
73 | return err
74 | }
75 |
76 | return nil
77 | }
78 |
79 | func formUrl(scheme, username, password, host, port string) string {
80 | var u = url.URL{
81 | Scheme: scheme,
82 | User: url.UserPassword(username, password),
83 | Host: net.JoinHostPort(host, port),
84 | }
85 | return u.String()
86 | }
87 |
--------------------------------------------------------------------------------
/services/gateway/transport/rest/handler/images.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "github.com/gin-contrib/cors"
5 | "github.com/prometheus/client_golang/prometheus/promhttp"
6 | "github.com/rvinnie/lightstream/services/gateway/monitoring"
7 | "github.com/rvinnie/lightstream/services/gateway/transport/amqp"
8 | "io/ioutil"
9 | "net/http"
10 | "path/filepath"
11 | "strconv"
12 | "time"
13 | "unicode"
14 |
15 | "github.com/gin-gonic/gin"
16 | "github.com/rvinnie/lightstream/services/gateway/config"
17 | pb "github.com/rvinnie/lightstream/services/gateway/pb"
18 | "github.com/rvinnie/lightstream/services/gateway/service"
19 | "google.golang.org/grpc"
20 | )
21 |
22 | const (
23 | imagesDirectoryName = "images"
24 | )
25 |
26 | type ImagesHandler struct {
27 | imageStorageClient pb.ImageStorageClient
28 | imagesService *service.ImagesService
29 | rabbitProducer *amqp.Producer
30 | metrics *monitoring.Metrics
31 | }
32 |
33 | func NewImagesHandler(grpcConn grpc.ClientConnInterface, imagesService *service.ImagesService, rabbitProducer *amqp.Producer, metrics *monitoring.Metrics) *ImagesHandler {
34 | return &ImagesHandler{
35 | imageStorageClient: pb.NewImageStorageClient(grpcConn),
36 | imagesService: imagesService,
37 | rabbitProducer: rabbitProducer,
38 | metrics: metrics,
39 | }
40 | }
41 |
42 | func PrometheusMiddleware(metrics *monitoring.Metrics) gin.HandlerFunc {
43 | return func(c *gin.Context) {
44 | start := time.Now()
45 | metrics.ConcurrentRequests.Inc()
46 | method := c.Request.Method
47 | url := c.Request.URL.Path
48 |
49 | // Truncate resource ids
50 | if unicode.IsDigit(rune(url[len(url)-1])) {
51 | url = filepath.Dir(url)
52 | }
53 |
54 | c.Next()
55 |
56 | metrics.ConcurrentRequests.Dec()
57 | statusCode := c.Writer.Status()
58 | duration := time.Since(start).Seconds()
59 | metrics.CollectMetrics(method, url, statusCode, duration)
60 | }
61 | }
62 |
63 | func (h *ImagesHandler) InitRoutes(cfg config.Config) *gin.Engine {
64 | gin.SetMode(cfg.GIN.Mode)
65 | router := gin.New()
66 | router.Use(PrometheusMiddleware(h.metrics))
67 | router.Use(cors.New(cors.Config{
68 | AllowOrigins: []string{"*"},
69 | AllowMethods: []string{"POST", "GET"},
70 | AllowHeaders: []string{"Origin", "Authorization", "Content-Type", "Accept-Encoding", "Filename"},
71 | }))
72 |
73 | router.GET("/metrics", gin.WrapH(promhttp.Handler()))
74 | router.GET("/ping", func(c *gin.Context) {
75 | c.String(http.StatusOK, "pong")
76 | })
77 |
78 | router.GET("/images/:path", h.getImage)
79 | router.GET("/images", h.getImages)
80 | router.POST("/images/add", h.createImage)
81 |
82 | return router
83 | }
84 |
85 | type imageResponse struct {
86 | Name string `json:"name"`
87 | ContentType string `json:"contentType"`
88 | Data []byte `json:"data"`
89 | }
90 |
91 | func (h *ImagesHandler) createImage(c *gin.Context) {
92 | data, err := ioutil.ReadAll(c.Request.Body)
93 |
94 | if err != nil {
95 | c.AbortWithStatus(http.StatusBadRequest)
96 | return
97 | }
98 |
99 | contentType := c.Request.Header.Get("Content-Type")
100 | if contentType == "" {
101 | c.AbortWithStatus(http.StatusBadRequest)
102 | return
103 | }
104 |
105 | filename := c.Request.Header.Get("Filename")
106 | if filename == "" {
107 | c.AbortWithStatus(http.StatusBadRequest)
108 | return
109 | }
110 |
111 | path := imagesDirectoryName + "/" + filename
112 | id, err := h.imagesService.Create(c, path)
113 | if err != nil {
114 | c.AbortWithStatus(http.StatusInternalServerError)
115 | return
116 | }
117 |
118 | _, err = h.imageStorageClient.CreateImage(c, &pb.CreateImageRequest{
119 | Path: path,
120 | ContentType: contentType,
121 | Image: data,
122 | })
123 | if err != nil {
124 | c.AbortWithStatus(http.StatusInternalServerError)
125 | return
126 | }
127 |
128 | c.JSON(http.StatusCreated, id)
129 | }
130 |
131 | func (h *ImagesHandler) getImage(c *gin.Context) {
132 | param := c.Param("path")
133 | id, err := strconv.Atoi(param)
134 | if err != nil {
135 | c.AbortWithStatus(http.StatusBadRequest)
136 | return
137 | }
138 |
139 | path, err := h.imagesService.GetById(c, id)
140 | if err != nil {
141 | c.AbortWithStatus(http.StatusNotFound)
142 | return
143 | }
144 |
145 | resp, err := h.imageStorageClient.GetImage(c, &pb.FindImageRequest{Path: path})
146 | if err != nil {
147 | c.AbortWithStatus(http.StatusNotFound)
148 | return
149 | }
150 |
151 | err = h.rabbitProducer.Publish(id)
152 | if err != nil {
153 | c.AbortWithStatus(http.StatusInternalServerError)
154 | return
155 | }
156 |
157 | image := imageResponse{
158 | Name: resp.Name,
159 | ContentType: resp.ContentType,
160 | Data: resp.Image,
161 | }
162 |
163 | c.JSON(http.StatusOK, image)
164 | }
165 |
166 | func (h *ImagesHandler) getImages(c *gin.Context) {
167 | paths, err := h.imagesService.GetAll(c)
168 | if err != nil {
169 | c.AbortWithStatus(http.StatusInternalServerError)
170 | return
171 | }
172 |
173 | resp, err := h.imageStorageClient.GetImages(c, &pb.FindImagesRequest{Paths: paths})
174 | if err != nil {
175 | c.AbortWithStatus(http.StatusNotFound)
176 | return
177 | }
178 |
179 | var images []imageResponse
180 | for _, image := range resp.GetImages() {
181 | images = append(images, imageResponse{
182 | Name: image.Name,
183 | ContentType: image.ContentType,
184 | Data: image.Image,
185 | })
186 | }
187 |
188 | if images == nil {
189 | c.JSON(http.StatusOK, []imageResponse{})
190 | return
191 | }
192 | c.JSON(http.StatusOK, images)
193 | }
194 |
--------------------------------------------------------------------------------
/services/gateway/transport/rest/server.go:
--------------------------------------------------------------------------------
1 | package rest
2 |
3 | import (
4 | "context"
5 | "github.com/rvinnie/lightstream/services/gateway/config"
6 | "net/http"
7 | )
8 |
9 | type Server struct {
10 | httpServer *http.Server
11 | }
12 |
13 | func NewServer(cfg *config.Config, handler http.Handler) *Server {
14 | return &Server{
15 | httpServer: &http.Server{
16 | Addr: cfg.HTTP.Host + ":" + cfg.HTTP.Port,
17 | Handler: handler,
18 | ReadTimeout: cfg.HTTP.ReadTimeout,
19 | WriteTimeout: cfg.HTTP.WriteTimeout,
20 | },
21 | }
22 | }
23 |
24 | func (s *Server) Run() error {
25 | return s.httpServer.ListenAndServe()
26 | }
27 |
28 | func (s *Server) Stop(ctx context.Context) error {
29 | return s.httpServer.Shutdown(ctx)
30 | }
31 |
--------------------------------------------------------------------------------
/services/history/Makefile:
--------------------------------------------------------------------------------
1 | include .env
2 |
3 | HOST=localhost
4 | PORT=5433
5 |
6 | build:
7 | go mod download && go build -o ./.bin/app ./cmd/main.go
8 |
9 | run: build
10 | ./.bin/app
11 |
12 | select:
13 | psql postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${HOST}:${PORT}/${POSTGRES_DB} -c 'SELECT * FROM notifications'
14 |
15 | .DEFAULT_GOAL := run
16 | .PHONY: build, run, select
--------------------------------------------------------------------------------
/services/history/cmd/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | postgres "github.com/rvinnie/lightstream/pkg/database"
5 | "github.com/rvinnie/lightstream/services/history/repository"
6 | "github.com/rvinnie/lightstream/services/history/service"
7 | "github.com/rvinnie/lightstream/services/history/transport/amqp"
8 | "os"
9 | "os/signal"
10 | "syscall"
11 |
12 | "github.com/joho/godotenv"
13 | "github.com/rvinnie/lightstream/services/history/config"
14 | "github.com/sirupsen/logrus"
15 | )
16 |
17 | const (
18 | configPath = "./config"
19 | )
20 |
21 | func main() {
22 | // Adding logger
23 | logrus.SetFormatter(new(logrus.JSONFormatter))
24 |
25 | // Initializing env variables
26 | if err := godotenv.Load(); err != nil {
27 | logrus.Fatal("Error loading .env file")
28 | }
29 |
30 | // Initializing config
31 | cfg, err := config.InitConfig(configPath)
32 | if err != nil {
33 | logrus.Fatal("Unable to parse config", err)
34 | }
35 |
36 | // Initializing postgres
37 | db, err := postgres.NewConnPool(postgres.DBConfig{
38 | Username: cfg.Postgres.Username,
39 | Password: cfg.Postgres.Password,
40 | Host: cfg.Postgres.Host,
41 | Port: cfg.Postgres.Port,
42 | DBName: cfg.Postgres.DBName,
43 | })
44 | if err != nil {
45 | logrus.Errorf("Unable to connect db: %v", err)
46 | return
47 | }
48 | defer db.Close()
49 |
50 | notificationsRepository := repository.NewNotificationsPostgres(db)
51 | notificationsService := service.NewNotificationsService(notificationsRepository)
52 |
53 | // Initializing RabbitMQ consumer
54 | consumer, err := amqp.NewConsumer(amqp.ConsumerConfig{
55 | Username: cfg.RabbitMQ.Username,
56 | Password: cfg.RabbitMQ.Password,
57 | Host: cfg.RabbitMQ.Host,
58 | Port: cfg.RabbitMQ.Port,
59 | }, notificationsService)
60 | if err != nil {
61 | logrus.Errorf("Unable to create RabbitMQ consumer: %v", err)
62 | return
63 | }
64 |
65 | if err = consumer.Consume(); err != nil {
66 | logrus.Fatal("Consuming failed: ", err)
67 | }
68 | logrus.Info("History (RabbitMQ) consumer is running")
69 |
70 | // Gracefull shutdown
71 | quit := make(chan os.Signal, 1)
72 | signal.Notify(quit, os.Interrupt, syscall.SIGQUIT, syscall.SIGTERM)
73 |
74 | <-quit
75 |
76 | logrus.Info("History (RabbitMQ) consumer shutting down")
77 | if err = consumer.Shutdown(); err != nil {
78 | logrus.Errorf("Error on history (RabbitMQ) consumer shutting down: %s", err.Error())
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/services/history/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "github.com/spf13/viper"
5 | "os"
6 | )
7 |
8 | type Config struct {
9 | Postgres PostgresConfig
10 | RabbitMQ RabbitMQConfig
11 | }
12 |
13 | type PostgresConfig struct {
14 | Username string
15 | Password string
16 | Host string
17 | Port string
18 | DBName string
19 | }
20 |
21 | type RabbitMQConfig struct {
22 | Username string
23 | Password string
24 | Host string `yaml:"host"`
25 | Port string `yaml:"port"`
26 | }
27 |
28 | func InitConfig(configDir string) (*Config, error) {
29 | viper.AddConfigPath(configDir)
30 | viper.SetConfigName("history")
31 | if err := viper.ReadInConfig(); err != nil {
32 | return nil, err
33 | }
34 |
35 | var cfg Config
36 | if err := viper.UnmarshalKey("rabbit", &cfg.RabbitMQ); err != nil {
37 | return nil, err
38 | }
39 |
40 | setEnvVariables(&cfg)
41 |
42 | return &cfg, nil
43 | }
44 |
45 | func setEnvVariables(cfg *Config) {
46 | cfg.Postgres.Username = os.Getenv("POSTGRES_USER")
47 | cfg.Postgres.Password = os.Getenv("POSTGRES_PASSWORD")
48 | cfg.Postgres.Host = os.Getenv("DATABASE_HOST")
49 | cfg.Postgres.DBName = os.Getenv("POSTGRES_DB")
50 |
51 | cfg.RabbitMQ.Username = os.Getenv("RABBIT_USER")
52 | cfg.RabbitMQ.Password = os.Getenv("RABBIT_PASSWORD")
53 | }
54 |
--------------------------------------------------------------------------------
/services/history/config/history.yaml:
--------------------------------------------------------------------------------
1 | rabbit:
2 | host: rabbit
3 | port: 5672
--------------------------------------------------------------------------------
/services/history/dev.Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.19-alpine
2 |
3 | WORKDIR /usr/src/app
4 |
5 | ENV CGO_ENABLED=0
6 |
7 | COPY ./ ./
8 |
9 | RUN apk add --no-cache make \
10 | && go mod download \
11 | && go get github.com/githubnemo/CompileDaemon \
12 | && go install github.com/githubnemo/CompileDaemon
13 |
14 | ENTRYPOINT CompileDaemon -build="go build -o ./.bin/app ./cmd/main.go" -command="./.bin/app"
--------------------------------------------------------------------------------
/services/history/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/rvinnie/lightstream/services/history
2 |
3 | go 1.18
4 |
5 | require (
6 | github.com/fsnotify/fsnotify v1.6.0 // indirect
7 | github.com/hashicorp/hcl v1.0.0 // indirect
8 | github.com/jackc/pgpassfile v1.0.0 // indirect
9 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
10 | github.com/jackc/pgx/v5 v5.3.1 // indirect
11 | github.com/jackc/puddle/v2 v2.2.0 // indirect
12 | github.com/joho/godotenv v1.5.1 // indirect
13 | github.com/magiconair/properties v1.8.7 // indirect
14 | github.com/mitchellh/mapstructure v1.5.0 // indirect
15 | github.com/pelletier/go-toml/v2 v2.0.8 // indirect
16 | github.com/rabbitmq/amqp091-go v1.8.1 // indirect
17 | github.com/rvinnie/lightstream/pkg v0.0.0-20230531140318-b669ba62628a // indirect
18 | github.com/sirupsen/logrus v1.9.2 // indirect
19 | github.com/spf13/afero v1.9.5 // indirect
20 | github.com/spf13/cast v1.5.1 // indirect
21 | github.com/spf13/jwalterweatherman v1.1.0 // indirect
22 | github.com/spf13/pflag v1.0.5 // indirect
23 | github.com/spf13/viper v1.16.0 // indirect
24 | github.com/subosito/gotenv v1.4.2 // indirect
25 | golang.org/x/crypto v0.9.0 // indirect
26 | golang.org/x/sync v0.1.0 // indirect
27 | golang.org/x/sys v0.8.0 // indirect
28 | golang.org/x/text v0.9.0 // indirect
29 | gopkg.in/ini.v1 v1.67.0 // indirect
30 | gopkg.in/yaml.v3 v3.0.1 // indirect
31 | )
32 |
--------------------------------------------------------------------------------
/services/history/go.sum:
--------------------------------------------------------------------------------
1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
6 | cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
7 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
8 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
9 | cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
10 | cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
11 | cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
12 | cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
13 | cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
14 | cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
15 | cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
16 | cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
17 | cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
18 | cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
19 | cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
20 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
21 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
22 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
23 | cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
24 | cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
25 | cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
26 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
27 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
28 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
29 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
30 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
31 | cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
32 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
33 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
34 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
35 | cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
36 | cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
37 | cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
38 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
39 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
40 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
41 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
42 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
43 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
44 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
45 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
46 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
47 | github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
48 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
49 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
50 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
51 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
52 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
53 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
54 | github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
55 | github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
56 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
57 | github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
58 | github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
59 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
60 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
61 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
62 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
63 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
64 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
65 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
66 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
67 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
68 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
69 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
70 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
71 | github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
72 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
73 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
74 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
75 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
76 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
77 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
78 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
79 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
80 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
81 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
82 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
83 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
84 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
85 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
86 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
87 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
88 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
89 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
90 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
91 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
92 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
93 | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
94 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
95 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
96 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
97 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
98 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
99 | github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
100 | github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
101 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
102 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
103 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
104 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
105 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
106 | github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
107 | github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
108 | github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
109 | github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
110 | github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
111 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
112 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
113 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
114 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
115 | github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
116 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
117 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
118 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
119 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
120 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
121 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
122 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
123 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
124 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
125 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
126 | github.com/jackc/pgx/v5 v5.3.1 h1:Fcr8QJ1ZeLi5zsPZqQeUZhNhxfkkKBOgJuYkJHoBOtU=
127 | github.com/jackc/pgx/v5 v5.3.1/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8=
128 | github.com/jackc/puddle/v2 v2.2.0 h1:RdcDk92EJBuBS55nQMMYFXTxwstHug4jkhT5pq8VxPk=
129 | github.com/jackc/puddle/v2 v2.2.0/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
130 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
131 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
132 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
133 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
134 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
135 | github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
136 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
137 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
138 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
139 | github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
140 | github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
141 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
142 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
143 | github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
144 | github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
145 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
146 | github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
147 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
148 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
149 | github.com/rabbitmq/amqp091-go v1.8.1 h1:RejT1SBUim5doqcL6s7iN6SBmsQqyTgXb1xMlH0h1hA=
150 | github.com/rabbitmq/amqp091-go v1.8.1/go.mod h1:+jPrT9iY2eLjRaMSRHUhc3z14E/l85kv/f+6luSD3pc=
151 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
152 | github.com/rvinnie/lightstream/pkg v0.0.0-20230531140318-b669ba62628a h1:e4ZSd/t/wNFWtW/3bsvtPfFfZdGhP+D9fXWhW/s172g=
153 | github.com/rvinnie/lightstream/pkg v0.0.0-20230531140318-b669ba62628a/go.mod h1:0IXuX6JbDlO1xDI/Eo61Ww4jTlFDHd3muCjjj2r8o8g=
154 | github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y=
155 | github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
156 | github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
157 | github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
158 | github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
159 | github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
160 | github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
161 | github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
162 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
163 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
164 | github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc=
165 | github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg=
166 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
167 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
168 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
169 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
170 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
171 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
172 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
173 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
174 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
175 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
176 | github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
177 | github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
178 | github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
179 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
180 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
181 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
182 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
183 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
184 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
185 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
186 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
187 | go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
188 | go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
189 | go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
190 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
191 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
192 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
193 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
194 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
195 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
196 | golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
197 | golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
198 | golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
199 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
200 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
201 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
202 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
203 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
204 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
205 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
206 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
207 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
208 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
209 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
210 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
211 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
212 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
213 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
214 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
215 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
216 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
217 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
218 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
219 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
220 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
221 | golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
222 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
223 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
224 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
225 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
226 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
227 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
228 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
229 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
230 | golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
231 | golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
232 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
233 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
234 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
235 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
236 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
237 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
238 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
239 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
240 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
241 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
242 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
243 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
244 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
245 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
246 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
247 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
248 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
249 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
250 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
251 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
252 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
253 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
254 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
255 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
256 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
257 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
258 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
259 | golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
260 | golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
261 | golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
262 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
263 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
264 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
265 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
266 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
267 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
268 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
269 | golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
270 | golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
271 | golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
272 | golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
273 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
274 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
275 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
276 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
277 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
278 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
279 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
280 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
281 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
282 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
283 | golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
284 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
285 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
286 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
287 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
288 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
289 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
290 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
291 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
292 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
293 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
294 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
295 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
296 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
297 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
298 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
299 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
300 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
301 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
302 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
303 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
304 | golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
305 | golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
306 | golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
307 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
308 | golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
309 | golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
310 | golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
311 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
312 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
313 | golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
314 | golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
315 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
316 | golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
317 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
318 | golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
319 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
320 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
321 | golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
322 | golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
323 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
324 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
325 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
326 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
327 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
328 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
329 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
330 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
331 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
332 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
333 | golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
334 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
335 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
336 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
337 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
338 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
339 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
340 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
341 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
342 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
343 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
344 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
345 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
346 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
347 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
348 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
349 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
350 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
351 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
352 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
353 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
354 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
355 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
356 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
357 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
358 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
359 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
360 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
361 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
362 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
363 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
364 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
365 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
366 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
367 | golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
368 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
369 | golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
370 | golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
371 | golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
372 | golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
373 | golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
374 | golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
375 | golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
376 | golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
377 | golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
378 | golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
379 | golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
380 | golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
381 | golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
382 | golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
383 | golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
384 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
385 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
386 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
387 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
388 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
389 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
390 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
391 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
392 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
393 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
394 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
395 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
396 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
397 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
398 | google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
399 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
400 | google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
401 | google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
402 | google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
403 | google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
404 | google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
405 | google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
406 | google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
407 | google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
408 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
409 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
410 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
411 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
412 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
413 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
414 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
415 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
416 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
417 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
418 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
419 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
420 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
421 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
422 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
423 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
424 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
425 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
426 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
427 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
428 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
429 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
430 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
431 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
432 | google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
433 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
434 | google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
435 | google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
436 | google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
437 | google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
438 | google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
439 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
440 | google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
441 | google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
442 | google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
443 | google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
444 | google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
445 | google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
446 | google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
447 | google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
448 | google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
449 | google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
450 | google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
451 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
452 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
453 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
454 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
455 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
456 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
457 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
458 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
459 | google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
460 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
461 | google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
462 | google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
463 | google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
464 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
465 | google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
466 | google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
467 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
468 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
469 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
470 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
471 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
472 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
473 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
474 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
475 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
476 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
477 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
478 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
479 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
480 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
481 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
482 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
483 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
484 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
485 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
486 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
487 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
488 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
489 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
490 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
491 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
492 | honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
493 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
494 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
495 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
496 |
--------------------------------------------------------------------------------
/services/history/prod.Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.19-alpine
2 |
3 | WORKDIR /usr/src/app
4 |
5 | ENV CGO_ENABLED=0
6 |
7 | COPY ./ ./
8 |
9 | RUN apk add --no-cache make && go mod download
10 |
11 | ENTRYPOINT go build -o ./.bin/app ./cmd/main.go && ./.bin/app
--------------------------------------------------------------------------------
/services/history/repository/notifications_postgres.go:
--------------------------------------------------------------------------------
1 | package repository
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "github.com/jackc/pgx/v5/pgxpool"
7 | )
8 |
9 | const (
10 | notificationsCollection = "notifications"
11 | )
12 |
13 | type Notifications interface {
14 | Create(ctx context.Context, videoId string) (int, error)
15 | }
16 |
17 | type NotificationsPostgres struct {
18 | db *pgxpool.Pool
19 | }
20 |
21 | func NewNotificationsPostgres(db *pgxpool.Pool) *NotificationsPostgres {
22 | return &NotificationsPostgres{db: db}
23 | }
24 |
25 | func (r *NotificationsPostgres) Create(ctx context.Context, videoId string) (int, error) {
26 | query := fmt.Sprintf("INSERT INTO %s (videoId) VALUES ($1) RETURNING id", notificationsCollection)
27 | row := r.db.QueryRow(ctx, query, videoId)
28 |
29 | var id int
30 | if err := row.Scan(&id); err != nil {
31 | return 0, err
32 | }
33 |
34 | return id, nil
35 | }
36 |
--------------------------------------------------------------------------------
/services/history/service/notifications.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "context"
5 | "github.com/rvinnie/lightstream/services/history/repository"
6 | )
7 |
8 | type Notifications interface {
9 | Create(ctx context.Context, videoId string) (int, error)
10 | }
11 |
12 | type NotificationsService struct {
13 | repo repository.Notifications
14 | }
15 |
16 | func NewNotificationsService(repo repository.Notifications) *NotificationsService {
17 | return &NotificationsService{repo: repo}
18 | }
19 |
20 | func (s *NotificationsService) Create(ctx context.Context, videoId string) (int, error) {
21 | return s.repo.Create(ctx, videoId)
22 | }
23 |
--------------------------------------------------------------------------------
/services/history/transport/amqp/consumer.go:
--------------------------------------------------------------------------------
1 | package amqp
2 |
3 | import (
4 | "context"
5 | "github.com/rabbitmq/amqp091-go"
6 | "github.com/rvinnie/lightstream/services/history/service"
7 | "net"
8 | "net/url"
9 | )
10 |
11 | const (
12 | exchangeName = "history"
13 | exchangeKind = "fanout"
14 | queueName = ""
15 | )
16 |
17 | type ConsumerConfig struct {
18 | Username string
19 | Password string
20 | Host string
21 | Port string
22 | }
23 |
24 | type Consumer struct {
25 | conn *amqp091.Connection
26 | ch *amqp091.Channel
27 |
28 | notificationsService service.Notifications
29 | }
30 |
31 | func NewConsumer(config ConsumerConfig, notificationsService service.Notifications) (*Consumer, error) {
32 | var err error
33 | c := &Consumer{}
34 |
35 | c.notificationsService = notificationsService
36 |
37 | url := formUrl("amqp", config.Username, config.Password, config.Host, config.Port)
38 |
39 | c.conn, err = amqp091.Dial(url)
40 | if err != nil {
41 | return c, err
42 | }
43 |
44 | c.ch, err = c.conn.Channel()
45 | if err != nil {
46 | return c, err
47 | }
48 |
49 | err = c.ch.ExchangeDeclare(exchangeName, exchangeKind, true, false, false, false, nil)
50 | if err != nil {
51 | return c, err
52 | }
53 |
54 | _, err = c.ch.QueueDeclare(
55 | queueName,
56 | false,
57 | false,
58 | true,
59 | false,
60 | nil,
61 | )
62 | if err != nil {
63 | return c, err
64 | }
65 |
66 | err = c.ch.QueueBind(
67 | queueName,
68 | "",
69 | exchangeName,
70 | false,
71 | nil,
72 | )
73 | if err != nil {
74 | return c, err
75 | }
76 |
77 | return c, err
78 | }
79 |
80 | func (c *Consumer) Consume() error {
81 | deliveries, err := c.ch.Consume(
82 | queueName,
83 | "",
84 | true,
85 | false,
86 | false,
87 | false,
88 | nil,
89 | )
90 | if err != nil {
91 | return err
92 | }
93 |
94 | go func() {
95 | for d := range deliveries {
96 | c.notificationsService.Create(context.Background(), string(d.Body))
97 | }
98 | }()
99 |
100 | return nil
101 | }
102 |
103 | func (c *Consumer) Shutdown() error {
104 | if err := c.ch.Close(); err != nil {
105 | return err
106 | }
107 |
108 | if err := c.conn.Close(); err != nil {
109 | return err
110 | }
111 |
112 | return nil
113 | }
114 |
115 | func formUrl(scheme, username, password, host, port string) string {
116 | var u = url.URL{
117 | Scheme: scheme,
118 | User: url.UserPassword(username, password),
119 | Host: net.JoinHostPort(host, port),
120 | }
121 | return u.String()
122 | }
123 |
--------------------------------------------------------------------------------
/services/storage/Makefile:
--------------------------------------------------------------------------------
1 | PATH_TO_PROTO = "../../api/proto/v1"
2 |
3 | build:
4 | go mod download && go build -o ./.bin/app ./cmd/main.go
5 |
6 | run: build
7 | ./.bin/app
8 |
9 | proto:
10 | protoc \
11 | --go_out=pb \
12 | --go_opt=paths=source_relative \
13 | --go-grpc_out=pb \
14 | --go-grpc_opt=paths=source_relative \
15 | --proto_path=$(PATH_TO_PROTO) $(PATH_TO_PROTO)/*.proto
16 |
17 | .DEFAULT_GOAL := run
18 | .PHONY: build, run
--------------------------------------------------------------------------------
/services/storage/aws/aws.go:
--------------------------------------------------------------------------------
1 | package aws
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "github.com/aws/aws-sdk-go-v2/aws"
7 | "github.com/aws/aws-sdk-go-v2/service/s3"
8 | "io"
9 | )
10 |
11 | type AWS interface {
12 | DownloadObject(path string) (*AWSStorageObject, error)
13 | DownloadObjects(paths []string) ([]*AWSStorageObject, error)
14 | UploadObject(filename string, contentType string, data []byte) error
15 | }
16 |
17 | type AWSManager struct {
18 | bucketName string
19 | client *s3.Client
20 | }
21 |
22 | type AWSStorageObject struct {
23 | Name string
24 | ContentType string
25 | Body []byte
26 | }
27 |
28 | func NewAWSManager(bucketName string, awsCfg aws.Config) *AWSManager {
29 | client := s3.NewFromConfig(awsCfg)
30 |
31 | return &AWSManager{
32 | bucketName: bucketName,
33 | client: client,
34 | }
35 | }
36 |
37 | func (m *AWSManager) DownloadObject(path string) (*AWSStorageObject, error) {
38 | object, err := m.client.GetObject(context.TODO(), &s3.GetObjectInput{
39 | Bucket: aws.String(m.bucketName),
40 | Key: aws.String(path),
41 | })
42 | if err != nil {
43 | return &AWSStorageObject{}, err
44 | }
45 |
46 | defer object.Body.Close()
47 |
48 | body, err := io.ReadAll(object.Body)
49 |
50 | return &AWSStorageObject{
51 | Name: path,
52 | ContentType: *object.ContentType,
53 | Body: body,
54 | }, err
55 | }
56 |
57 | func (m *AWSManager) DownloadObjects(paths []string) ([]*AWSStorageObject, error) {
58 | var outputObjects []*AWSStorageObject
59 |
60 | for _, path := range paths {
61 | outputObject, err := m.DownloadObject(path)
62 | if err != nil {
63 | return []*AWSStorageObject{}, err
64 | }
65 | outputObjects = append(outputObjects, outputObject)
66 | }
67 |
68 | return outputObjects, nil
69 | }
70 |
71 | func (m *AWSManager) UploadObject(path string, contentType string, data []byte) error {
72 | body := bytes.NewReader(data)
73 |
74 | _, err := m.client.PutObject(context.TODO(), &s3.PutObjectInput{
75 | Bucket: aws.String(m.bucketName),
76 | Key: aws.String(path),
77 | ContentType: aws.String(contentType),
78 | Body: body,
79 | })
80 |
81 | return err
82 | }
83 |
--------------------------------------------------------------------------------
/services/storage/cmd/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "os"
5 | "os/signal"
6 | "syscall"
7 |
8 | "github.com/rvinnie/lightstream/services/storage/aws"
9 | "github.com/rvinnie/lightstream/services/storage/config"
10 | "github.com/rvinnie/lightstream/services/storage/transport/grpc"
11 | "github.com/rvinnie/lightstream/services/storage/transport/grpc/handler"
12 |
13 | "github.com/joho/godotenv"
14 | "github.com/sirupsen/logrus"
15 | )
16 |
17 | const (
18 | configPath = "./config"
19 | )
20 |
21 | func main() {
22 | // Adding logger
23 | logrus.SetFormatter(new(logrus.JSONFormatter))
24 |
25 | // Initializing env variables
26 | if err := godotenv.Load(); err != nil {
27 | logrus.Fatal("Error loading .env file")
28 | }
29 |
30 | //Initializing config
31 | cfg, err := config.InitConfig(configPath)
32 | if err != nil {
33 | logrus.Fatal("Unable to parse config", err)
34 | }
35 |
36 | // Creating AWS manager
37 | awsManager := aws.NewAWSManager(cfg.AWS.BucketName, cfg.AWS.Config)
38 |
39 | // Creating handlers
40 | grpcHandler := handler.NewImageStorageHandler(awsManager, cfg.AWS)
41 |
42 | // Creating gRPC server
43 | grpcServer := grpc.NewServer(grpcHandler)
44 | go func() {
45 | if err = grpcServer.ListenAndServe(cfg.GRPC.Port); err != nil {
46 | logrus.Fatalf("error occured while running storage (gRPC) server: %s", err.Error())
47 | }
48 | }()
49 | logrus.Info("Storage (gRPC) server is running")
50 |
51 | // Gracefull shutdown
52 | quit := make(chan os.Signal, 1)
53 | signal.Notify(quit, os.Interrupt, syscall.SIGQUIT, syscall.SIGTERM)
54 |
55 | <-quit
56 | logrus.Info("Storage (gRPC) server shutting down")
57 |
58 | grpcServer.Stop()
59 | }
60 |
--------------------------------------------------------------------------------
/services/storage/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | "github.com/aws/aws-sdk-go-v2/aws"
8 | "github.com/aws/aws-sdk-go-v2/config"
9 | "github.com/aws/aws-sdk-go-v2/service/s3"
10 | "github.com/spf13/viper"
11 | )
12 |
13 | type Config struct {
14 | GRPC GRPCConfig
15 | AWS AWSConfig
16 | }
17 |
18 | type GRPCConfig struct {
19 | Port string `yaml:"port"`
20 | }
21 |
22 | type AWSConfig struct {
23 | BucketName string `yaml:"bucketName"`
24 | Config aws.Config
25 | }
26 |
27 | func InitConfig(configDir string) (*Config, error) {
28 | viper.AddConfigPath(configDir)
29 | viper.SetConfigName("storage")
30 | if err := viper.ReadInConfig(); err != nil {
31 | return nil, err
32 | }
33 |
34 | var cfg Config
35 | if err := viper.UnmarshalKey("gRPC", &cfg.GRPC); err != nil {
36 | return nil, err
37 | }
38 |
39 | if err := viper.UnmarshalKey("aws", &cfg.AWS); err != nil {
40 | return nil, err
41 | }
42 |
43 | if err := loadAWSConfig(&cfg); err != nil {
44 | return nil, err
45 | }
46 |
47 | return &cfg, nil
48 | }
49 |
50 | func loadAWSConfig(cfg *Config) error {
51 | // Create a custom endpoint resolver
52 | customResolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) {
53 | if service == s3.ServiceID && region == "ru-central1" {
54 | return aws.Endpoint{
55 | PartitionID: "yc",
56 | URL: "https://storage.yandexcloud.net",
57 | SigningRegion: "ru-central1",
58 | }, nil
59 | }
60 | return aws.Endpoint{}, fmt.Errorf("unknown endpoint requested")
61 | })
62 |
63 | // Load config from ~/.aws/*
64 | awsCfg, err := config.LoadDefaultConfig(context.TODO(), config.WithEndpointResolverWithOptions(customResolver))
65 | if err != nil {
66 | return err
67 | }
68 |
69 | cfg.AWS.Config = awsCfg
70 |
71 | return err
72 | }
73 |
--------------------------------------------------------------------------------
/services/storage/config/storage.yaml:
--------------------------------------------------------------------------------
1 | gRPC:
2 | port: 4040
3 |
4 | aws:
5 | bucketName: "rvinnie-lightstream"
--------------------------------------------------------------------------------
/services/storage/dev.Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.19-alpine
2 |
3 | WORKDIR /usr/src/app
4 |
5 | ENV CGO_ENABLED=0
6 |
7 | COPY ./ ./
8 |
9 | RUN apk add --no-cache make \
10 | && go mod download \
11 | && go get github.com/githubnemo/CompileDaemon \
12 | && go install github.com/githubnemo/CompileDaemon
13 |
14 | ENTRYPOINT CompileDaemon -build="go build -o ./.bin/app ./cmd/main.go" -command="./.bin/app"
--------------------------------------------------------------------------------
/services/storage/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/rvinnie/lightstream/services/storage
2 |
3 | go 1.19
4 |
5 | require (
6 | github.com/aws/aws-sdk-go-v2 v1.18.0
7 | github.com/aws/aws-sdk-go-v2/config v1.18.25
8 | github.com/aws/aws-sdk-go-v2/service/s3 v1.33.1
9 | github.com/gin-gonic/gin v1.9.0
10 | github.com/joho/godotenv v1.5.1
11 | github.com/sirupsen/logrus v1.9.2
12 | github.com/spf13/viper v1.15.0
13 | )
14 |
15 | require (
16 | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect
17 | github.com/aws/aws-sdk-go-v2/credentials v1.13.24 // indirect
18 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3 // indirect
19 | github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.67 // indirect
20 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33 // indirect
21 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27 // indirect
22 | github.com/aws/aws-sdk-go-v2/internal/ini v1.3.34 // indirect
23 | github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.25 // indirect
24 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 // indirect
25 | github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.28 // indirect
26 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27 // indirect
27 | github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.2 // indirect
28 | github.com/aws/aws-sdk-go-v2/service/sso v1.12.10 // indirect
29 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.10 // indirect
30 | github.com/aws/aws-sdk-go-v2/service/sts v1.19.0 // indirect
31 | github.com/aws/smithy-go v1.13.5 // indirect
32 | github.com/bytedance/sonic v1.8.0 // indirect
33 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
34 | github.com/fsnotify/fsnotify v1.6.0 // indirect
35 | github.com/gin-contrib/sse v0.1.0 // indirect
36 | github.com/go-playground/locales v0.14.1 // indirect
37 | github.com/go-playground/universal-translator v0.18.1 // indirect
38 | github.com/go-playground/validator/v10 v10.11.2 // indirect
39 | github.com/goccy/go-json v0.10.0 // indirect
40 | github.com/golang/protobuf v1.5.3 // indirect
41 | github.com/hashicorp/hcl v1.0.0 // indirect
42 | github.com/jmespath/go-jmespath v0.4.0 // indirect
43 | github.com/json-iterator/go v1.1.12 // indirect
44 | github.com/klauspost/cpuid/v2 v2.0.9 // indirect
45 | github.com/leodido/go-urn v1.2.1 // indirect
46 | github.com/magiconair/properties v1.8.7 // indirect
47 | github.com/mattn/go-isatty v0.0.17 // indirect
48 | github.com/mitchellh/mapstructure v1.5.0 // indirect
49 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
50 | github.com/modern-go/reflect2 v1.0.2 // indirect
51 | github.com/pelletier/go-toml/v2 v2.0.6 // indirect
52 | github.com/spf13/afero v1.9.3 // indirect
53 | github.com/spf13/cast v1.5.0 // indirect
54 | github.com/spf13/jwalterweatherman v1.1.0 // indirect
55 | github.com/spf13/pflag v1.0.5 // indirect
56 | github.com/subosito/gotenv v1.4.2 // indirect
57 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
58 | github.com/ugorji/go/codec v1.2.9 // indirect
59 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
60 | golang.org/x/crypto v0.5.0 // indirect
61 | golang.org/x/net v0.10.0 // indirect
62 | golang.org/x/sys v0.8.0 // indirect
63 | golang.org/x/text v0.9.0 // indirect
64 | google.golang.org/genproto v0.0.0-20230525234025-438c736192d0 // indirect
65 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230526015343-6ee61e4f9d5f // indirect
66 | google.golang.org/grpc v1.55.0 // indirect
67 | google.golang.org/protobuf v1.30.0 // indirect
68 | gopkg.in/ini.v1 v1.67.0 // indirect
69 | gopkg.in/yaml.v3 v3.0.1 // indirect
70 | )
71 |
--------------------------------------------------------------------------------
/services/storage/pb/image_storage.pb.go:
--------------------------------------------------------------------------------
1 | // Code generated by protoc-gen-go. DO NOT EDIT.
2 | // versions:
3 | // protoc-gen-go v1.30.0
4 | // protoc v4.23.2
5 | // source: image_storage.proto
6 |
7 | package v1
8 |
9 | import (
10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect"
11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl"
12 | emptypb "google.golang.org/protobuf/types/known/emptypb"
13 | reflect "reflect"
14 | sync "sync"
15 | )
16 |
17 | const (
18 | // Verify that this generated code is sufficiently up-to-date.
19 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
20 | // Verify that runtime/protoimpl is sufficiently up-to-date.
21 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
22 | )
23 |
24 | type CreateImageRequest struct {
25 | state protoimpl.MessageState
26 | sizeCache protoimpl.SizeCache
27 | unknownFields protoimpl.UnknownFields
28 |
29 | Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"`
30 | ContentType string `protobuf:"bytes,2,opt,name=contentType,proto3" json:"contentType,omitempty"`
31 | Image []byte `protobuf:"bytes,3,opt,name=image,proto3" json:"image,omitempty"`
32 | }
33 |
34 | func (x *CreateImageRequest) Reset() {
35 | *x = CreateImageRequest{}
36 | if protoimpl.UnsafeEnabled {
37 | mi := &file_image_storage_proto_msgTypes[0]
38 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
39 | ms.StoreMessageInfo(mi)
40 | }
41 | }
42 |
43 | func (x *CreateImageRequest) String() string {
44 | return protoimpl.X.MessageStringOf(x)
45 | }
46 |
47 | func (*CreateImageRequest) ProtoMessage() {}
48 |
49 | func (x *CreateImageRequest) ProtoReflect() protoreflect.Message {
50 | mi := &file_image_storage_proto_msgTypes[0]
51 | if protoimpl.UnsafeEnabled && x != nil {
52 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
53 | if ms.LoadMessageInfo() == nil {
54 | ms.StoreMessageInfo(mi)
55 | }
56 | return ms
57 | }
58 | return mi.MessageOf(x)
59 | }
60 |
61 | // Deprecated: Use CreateImageRequest.ProtoReflect.Descriptor instead.
62 | func (*CreateImageRequest) Descriptor() ([]byte, []int) {
63 | return file_image_storage_proto_rawDescGZIP(), []int{0}
64 | }
65 |
66 | func (x *CreateImageRequest) GetPath() string {
67 | if x != nil {
68 | return x.Path
69 | }
70 | return ""
71 | }
72 |
73 | func (x *CreateImageRequest) GetContentType() string {
74 | if x != nil {
75 | return x.ContentType
76 | }
77 | return ""
78 | }
79 |
80 | func (x *CreateImageRequest) GetImage() []byte {
81 | if x != nil {
82 | return x.Image
83 | }
84 | return nil
85 | }
86 |
87 | type FindImageRequest struct {
88 | state protoimpl.MessageState
89 | sizeCache protoimpl.SizeCache
90 | unknownFields protoimpl.UnknownFields
91 |
92 | Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"`
93 | }
94 |
95 | func (x *FindImageRequest) Reset() {
96 | *x = FindImageRequest{}
97 | if protoimpl.UnsafeEnabled {
98 | mi := &file_image_storage_proto_msgTypes[1]
99 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
100 | ms.StoreMessageInfo(mi)
101 | }
102 | }
103 |
104 | func (x *FindImageRequest) String() string {
105 | return protoimpl.X.MessageStringOf(x)
106 | }
107 |
108 | func (*FindImageRequest) ProtoMessage() {}
109 |
110 | func (x *FindImageRequest) ProtoReflect() protoreflect.Message {
111 | mi := &file_image_storage_proto_msgTypes[1]
112 | if protoimpl.UnsafeEnabled && x != nil {
113 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
114 | if ms.LoadMessageInfo() == nil {
115 | ms.StoreMessageInfo(mi)
116 | }
117 | return ms
118 | }
119 | return mi.MessageOf(x)
120 | }
121 |
122 | // Deprecated: Use FindImageRequest.ProtoReflect.Descriptor instead.
123 | func (*FindImageRequest) Descriptor() ([]byte, []int) {
124 | return file_image_storage_proto_rawDescGZIP(), []int{1}
125 | }
126 |
127 | func (x *FindImageRequest) GetPath() string {
128 | if x != nil {
129 | return x.Path
130 | }
131 | return ""
132 | }
133 |
134 | type FindImagesRequest struct {
135 | state protoimpl.MessageState
136 | sizeCache protoimpl.SizeCache
137 | unknownFields protoimpl.UnknownFields
138 |
139 | Paths []string `protobuf:"bytes,1,rep,name=paths,proto3" json:"paths,omitempty"`
140 | }
141 |
142 | func (x *FindImagesRequest) Reset() {
143 | *x = FindImagesRequest{}
144 | if protoimpl.UnsafeEnabled {
145 | mi := &file_image_storage_proto_msgTypes[2]
146 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
147 | ms.StoreMessageInfo(mi)
148 | }
149 | }
150 |
151 | func (x *FindImagesRequest) String() string {
152 | return protoimpl.X.MessageStringOf(x)
153 | }
154 |
155 | func (*FindImagesRequest) ProtoMessage() {}
156 |
157 | func (x *FindImagesRequest) ProtoReflect() protoreflect.Message {
158 | mi := &file_image_storage_proto_msgTypes[2]
159 | if protoimpl.UnsafeEnabled && x != nil {
160 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
161 | if ms.LoadMessageInfo() == nil {
162 | ms.StoreMessageInfo(mi)
163 | }
164 | return ms
165 | }
166 | return mi.MessageOf(x)
167 | }
168 |
169 | // Deprecated: Use FindImagesRequest.ProtoReflect.Descriptor instead.
170 | func (*FindImagesRequest) Descriptor() ([]byte, []int) {
171 | return file_image_storage_proto_rawDescGZIP(), []int{2}
172 | }
173 |
174 | func (x *FindImagesRequest) GetPaths() []string {
175 | if x != nil {
176 | return x.Paths
177 | }
178 | return nil
179 | }
180 |
181 | type FindImageResponse struct {
182 | state protoimpl.MessageState
183 | sizeCache protoimpl.SizeCache
184 | unknownFields protoimpl.UnknownFields
185 |
186 | Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
187 | ContentType string `protobuf:"bytes,2,opt,name=contentType,proto3" json:"contentType,omitempty"`
188 | Image []byte `protobuf:"bytes,3,opt,name=image,proto3" json:"image,omitempty"`
189 | }
190 |
191 | func (x *FindImageResponse) Reset() {
192 | *x = FindImageResponse{}
193 | if protoimpl.UnsafeEnabled {
194 | mi := &file_image_storage_proto_msgTypes[3]
195 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
196 | ms.StoreMessageInfo(mi)
197 | }
198 | }
199 |
200 | func (x *FindImageResponse) String() string {
201 | return protoimpl.X.MessageStringOf(x)
202 | }
203 |
204 | func (*FindImageResponse) ProtoMessage() {}
205 |
206 | func (x *FindImageResponse) ProtoReflect() protoreflect.Message {
207 | mi := &file_image_storage_proto_msgTypes[3]
208 | if protoimpl.UnsafeEnabled && x != nil {
209 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
210 | if ms.LoadMessageInfo() == nil {
211 | ms.StoreMessageInfo(mi)
212 | }
213 | return ms
214 | }
215 | return mi.MessageOf(x)
216 | }
217 |
218 | // Deprecated: Use FindImageResponse.ProtoReflect.Descriptor instead.
219 | func (*FindImageResponse) Descriptor() ([]byte, []int) {
220 | return file_image_storage_proto_rawDescGZIP(), []int{3}
221 | }
222 |
223 | func (x *FindImageResponse) GetName() string {
224 | if x != nil {
225 | return x.Name
226 | }
227 | return ""
228 | }
229 |
230 | func (x *FindImageResponse) GetContentType() string {
231 | if x != nil {
232 | return x.ContentType
233 | }
234 | return ""
235 | }
236 |
237 | func (x *FindImageResponse) GetImage() []byte {
238 | if x != nil {
239 | return x.Image
240 | }
241 | return nil
242 | }
243 |
244 | type FindImagesResponse struct {
245 | state protoimpl.MessageState
246 | sizeCache protoimpl.SizeCache
247 | unknownFields protoimpl.UnknownFields
248 |
249 | Images []*FindImageResponse `protobuf:"bytes,1,rep,name=images,proto3" json:"images,omitempty"`
250 | }
251 |
252 | func (x *FindImagesResponse) Reset() {
253 | *x = FindImagesResponse{}
254 | if protoimpl.UnsafeEnabled {
255 | mi := &file_image_storage_proto_msgTypes[4]
256 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
257 | ms.StoreMessageInfo(mi)
258 | }
259 | }
260 |
261 | func (x *FindImagesResponse) String() string {
262 | return protoimpl.X.MessageStringOf(x)
263 | }
264 |
265 | func (*FindImagesResponse) ProtoMessage() {}
266 |
267 | func (x *FindImagesResponse) ProtoReflect() protoreflect.Message {
268 | mi := &file_image_storage_proto_msgTypes[4]
269 | if protoimpl.UnsafeEnabled && x != nil {
270 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
271 | if ms.LoadMessageInfo() == nil {
272 | ms.StoreMessageInfo(mi)
273 | }
274 | return ms
275 | }
276 | return mi.MessageOf(x)
277 | }
278 |
279 | // Deprecated: Use FindImagesResponse.ProtoReflect.Descriptor instead.
280 | func (*FindImagesResponse) Descriptor() ([]byte, []int) {
281 | return file_image_storage_proto_rawDescGZIP(), []int{4}
282 | }
283 |
284 | func (x *FindImagesResponse) GetImages() []*FindImageResponse {
285 | if x != nil {
286 | return x.Images
287 | }
288 | return nil
289 | }
290 |
291 | var File_image_storage_proto protoreflect.FileDescriptor
292 |
293 | var file_image_storage_proto_rawDesc = []byte{
294 | 0x0a, 0x13, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e,
295 | 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x07, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x1a, 0x1b,
296 | 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f,
297 | 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x60, 0x0a, 0x12, 0x43,
298 | 0x72, 0x65, 0x61, 0x74, 0x65, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
299 | 0x74, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
300 | 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x20, 0x0a, 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74,
301 | 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x74,
302 | 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65,
303 | 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x22, 0x26, 0x0a,
304 | 0x10, 0x46, 0x69, 0x6e, 0x64, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
305 | 0x74, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
306 | 0x04, 0x70, 0x61, 0x74, 0x68, 0x22, 0x29, 0x0a, 0x11, 0x46, 0x69, 0x6e, 0x64, 0x49, 0x6d, 0x61,
307 | 0x67, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x70, 0x61,
308 | 0x74, 0x68, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x70, 0x61, 0x74, 0x68, 0x73,
309 | 0x22, 0x5f, 0x0a, 0x11, 0x46, 0x69, 0x6e, 0x64, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73,
310 | 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20,
311 | 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x63, 0x6f, 0x6e,
312 | 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b,
313 | 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x69,
314 | 0x6d, 0x61, 0x67, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x69, 0x6d, 0x61, 0x67,
315 | 0x65, 0x22, 0x48, 0x0a, 0x12, 0x46, 0x69, 0x6e, 0x64, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x73, 0x52,
316 | 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x32, 0x0a, 0x06, 0x69, 0x6d, 0x61, 0x67, 0x65,
317 | 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61,
318 | 0x79, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f,
319 | 0x6e, 0x73, 0x65, 0x52, 0x06, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x73, 0x32, 0xe1, 0x01, 0x0a, 0x0c,
320 | 0x49, 0x6d, 0x61, 0x67, 0x65, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x12, 0x44, 0x0a, 0x0b,
321 | 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x1b, 0x2e, 0x67, 0x61,
322 | 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x49, 0x6d, 0x61, 0x67,
323 | 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
324 | 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79,
325 | 0x22, 0x00, 0x12, 0x43, 0x0a, 0x08, 0x47, 0x65, 0x74, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x19,
326 | 0x2e, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x49, 0x6d, 0x61,
327 | 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x67, 0x61, 0x74, 0x65,
328 | 0x77, 0x61, 0x79, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73,
329 | 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x46, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x49, 0x6d,
330 | 0x61, 0x67, 0x65, 0x73, 0x12, 0x1a, 0x2e, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x46,
331 | 0x69, 0x6e, 0x64, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
332 | 0x1a, 0x1b, 0x2e, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x49,
333 | 0x6d, 0x61, 0x67, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42,
334 | 0x2d, 0x5a, 0x2b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x72, 0x76,
335 | 0x69, 0x6e, 0x6e, 0x69, 0x65, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x73, 0x74, 0x72, 0x65, 0x61,
336 | 0x6d, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06,
337 | 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
338 | }
339 |
340 | var (
341 | file_image_storage_proto_rawDescOnce sync.Once
342 | file_image_storage_proto_rawDescData = file_image_storage_proto_rawDesc
343 | )
344 |
345 | func file_image_storage_proto_rawDescGZIP() []byte {
346 | file_image_storage_proto_rawDescOnce.Do(func() {
347 | file_image_storage_proto_rawDescData = protoimpl.X.CompressGZIP(file_image_storage_proto_rawDescData)
348 | })
349 | return file_image_storage_proto_rawDescData
350 | }
351 |
352 | var file_image_storage_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
353 | var file_image_storage_proto_goTypes = []interface{}{
354 | (*CreateImageRequest)(nil), // 0: gateway.CreateImageRequest
355 | (*FindImageRequest)(nil), // 1: gateway.FindImageRequest
356 | (*FindImagesRequest)(nil), // 2: gateway.FindImagesRequest
357 | (*FindImageResponse)(nil), // 3: gateway.FindImageResponse
358 | (*FindImagesResponse)(nil), // 4: gateway.FindImagesResponse
359 | (*emptypb.Empty)(nil), // 5: google.protobuf.Empty
360 | }
361 | var file_image_storage_proto_depIdxs = []int32{
362 | 3, // 0: gateway.FindImagesResponse.images:type_name -> gateway.FindImageResponse
363 | 0, // 1: gateway.ImageStorage.CreateImage:input_type -> gateway.CreateImageRequest
364 | 1, // 2: gateway.ImageStorage.GetImage:input_type -> gateway.FindImageRequest
365 | 2, // 3: gateway.ImageStorage.GetImages:input_type -> gateway.FindImagesRequest
366 | 5, // 4: gateway.ImageStorage.CreateImage:output_type -> google.protobuf.Empty
367 | 3, // 5: gateway.ImageStorage.GetImage:output_type -> gateway.FindImageResponse
368 | 4, // 6: gateway.ImageStorage.GetImages:output_type -> gateway.FindImagesResponse
369 | 4, // [4:7] is the sub-list for method output_type
370 | 1, // [1:4] is the sub-list for method input_type
371 | 1, // [1:1] is the sub-list for extension type_name
372 | 1, // [1:1] is the sub-list for extension extendee
373 | 0, // [0:1] is the sub-list for field type_name
374 | }
375 |
376 | func init() { file_image_storage_proto_init() }
377 | func file_image_storage_proto_init() {
378 | if File_image_storage_proto != nil {
379 | return
380 | }
381 | if !protoimpl.UnsafeEnabled {
382 | file_image_storage_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
383 | switch v := v.(*CreateImageRequest); i {
384 | case 0:
385 | return &v.state
386 | case 1:
387 | return &v.sizeCache
388 | case 2:
389 | return &v.unknownFields
390 | default:
391 | return nil
392 | }
393 | }
394 | file_image_storage_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
395 | switch v := v.(*FindImageRequest); i {
396 | case 0:
397 | return &v.state
398 | case 1:
399 | return &v.sizeCache
400 | case 2:
401 | return &v.unknownFields
402 | default:
403 | return nil
404 | }
405 | }
406 | file_image_storage_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
407 | switch v := v.(*FindImagesRequest); i {
408 | case 0:
409 | return &v.state
410 | case 1:
411 | return &v.sizeCache
412 | case 2:
413 | return &v.unknownFields
414 | default:
415 | return nil
416 | }
417 | }
418 | file_image_storage_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
419 | switch v := v.(*FindImageResponse); i {
420 | case 0:
421 | return &v.state
422 | case 1:
423 | return &v.sizeCache
424 | case 2:
425 | return &v.unknownFields
426 | default:
427 | return nil
428 | }
429 | }
430 | file_image_storage_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
431 | switch v := v.(*FindImagesResponse); i {
432 | case 0:
433 | return &v.state
434 | case 1:
435 | return &v.sizeCache
436 | case 2:
437 | return &v.unknownFields
438 | default:
439 | return nil
440 | }
441 | }
442 | }
443 | type x struct{}
444 | out := protoimpl.TypeBuilder{
445 | File: protoimpl.DescBuilder{
446 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
447 | RawDescriptor: file_image_storage_proto_rawDesc,
448 | NumEnums: 0,
449 | NumMessages: 5,
450 | NumExtensions: 0,
451 | NumServices: 1,
452 | },
453 | GoTypes: file_image_storage_proto_goTypes,
454 | DependencyIndexes: file_image_storage_proto_depIdxs,
455 | MessageInfos: file_image_storage_proto_msgTypes,
456 | }.Build()
457 | File_image_storage_proto = out.File
458 | file_image_storage_proto_rawDesc = nil
459 | file_image_storage_proto_goTypes = nil
460 | file_image_storage_proto_depIdxs = nil
461 | }
462 |
--------------------------------------------------------------------------------
/services/storage/pb/image_storage_grpc.pb.go:
--------------------------------------------------------------------------------
1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
2 | // versions:
3 | // - protoc-gen-go-grpc v1.3.0
4 | // - protoc v4.23.2
5 | // source: image_storage.proto
6 |
7 | package v1
8 |
9 | import (
10 | context "context"
11 | grpc "google.golang.org/grpc"
12 | codes "google.golang.org/grpc/codes"
13 | status "google.golang.org/grpc/status"
14 | emptypb "google.golang.org/protobuf/types/known/emptypb"
15 | )
16 |
17 | // This is a compile-time assertion to ensure that this generated file
18 | // is compatible with the grpc package it is being compiled against.
19 | // Requires gRPC-Go v1.32.0 or later.
20 | const _ = grpc.SupportPackageIsVersion7
21 |
22 | const (
23 | ImageStorage_CreateImage_FullMethodName = "/gateway.ImageStorage/CreateImage"
24 | ImageStorage_GetImage_FullMethodName = "/gateway.ImageStorage/GetImage"
25 | ImageStorage_GetImages_FullMethodName = "/gateway.ImageStorage/GetImages"
26 | )
27 |
28 | // ImageStorageClient is the client API for ImageStorage service.
29 | //
30 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
31 | type ImageStorageClient interface {
32 | CreateImage(ctx context.Context, in *CreateImageRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
33 | GetImage(ctx context.Context, in *FindImageRequest, opts ...grpc.CallOption) (*FindImageResponse, error)
34 | GetImages(ctx context.Context, in *FindImagesRequest, opts ...grpc.CallOption) (*FindImagesResponse, error)
35 | }
36 |
37 | type imageStorageClient struct {
38 | cc grpc.ClientConnInterface
39 | }
40 |
41 | func NewImageStorageClient(cc grpc.ClientConnInterface) ImageStorageClient {
42 | return &imageStorageClient{cc}
43 | }
44 |
45 | func (c *imageStorageClient) CreateImage(ctx context.Context, in *CreateImageRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
46 | out := new(emptypb.Empty)
47 | err := c.cc.Invoke(ctx, ImageStorage_CreateImage_FullMethodName, in, out, opts...)
48 | if err != nil {
49 | return nil, err
50 | }
51 | return out, nil
52 | }
53 |
54 | func (c *imageStorageClient) GetImage(ctx context.Context, in *FindImageRequest, opts ...grpc.CallOption) (*FindImageResponse, error) {
55 | out := new(FindImageResponse)
56 | err := c.cc.Invoke(ctx, ImageStorage_GetImage_FullMethodName, in, out, opts...)
57 | if err != nil {
58 | return nil, err
59 | }
60 | return out, nil
61 | }
62 |
63 | func (c *imageStorageClient) GetImages(ctx context.Context, in *FindImagesRequest, opts ...grpc.CallOption) (*FindImagesResponse, error) {
64 | out := new(FindImagesResponse)
65 | err := c.cc.Invoke(ctx, ImageStorage_GetImages_FullMethodName, in, out, opts...)
66 | if err != nil {
67 | return nil, err
68 | }
69 | return out, nil
70 | }
71 |
72 | // ImageStorageServer is the server API for ImageStorage service.
73 | // All implementations must embed UnimplementedImageStorageServer
74 | // for forward compatibility
75 | type ImageStorageServer interface {
76 | CreateImage(context.Context, *CreateImageRequest) (*emptypb.Empty, error)
77 | GetImage(context.Context, *FindImageRequest) (*FindImageResponse, error)
78 | GetImages(context.Context, *FindImagesRequest) (*FindImagesResponse, error)
79 | mustEmbedUnimplementedImageStorageServer()
80 | }
81 |
82 | // UnimplementedImageStorageServer must be embedded to have forward compatible implementations.
83 | type UnimplementedImageStorageServer struct {
84 | }
85 |
86 | func (UnimplementedImageStorageServer) CreateImage(context.Context, *CreateImageRequest) (*emptypb.Empty, error) {
87 | return nil, status.Errorf(codes.Unimplemented, "method CreateImage not implemented")
88 | }
89 | func (UnimplementedImageStorageServer) GetImage(context.Context, *FindImageRequest) (*FindImageResponse, error) {
90 | return nil, status.Errorf(codes.Unimplemented, "method GetImage not implemented")
91 | }
92 | func (UnimplementedImageStorageServer) GetImages(context.Context, *FindImagesRequest) (*FindImagesResponse, error) {
93 | return nil, status.Errorf(codes.Unimplemented, "method GetImages not implemented")
94 | }
95 | func (UnimplementedImageStorageServer) mustEmbedUnimplementedImageStorageServer() {}
96 |
97 | // UnsafeImageStorageServer may be embedded to opt out of forward compatibility for this service.
98 | // Use of this interface is not recommended, as added methods to ImageStorageServer will
99 | // result in compilation errors.
100 | type UnsafeImageStorageServer interface {
101 | mustEmbedUnimplementedImageStorageServer()
102 | }
103 |
104 | func RegisterImageStorageServer(s grpc.ServiceRegistrar, srv ImageStorageServer) {
105 | s.RegisterService(&ImageStorage_ServiceDesc, srv)
106 | }
107 |
108 | func _ImageStorage_CreateImage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
109 | in := new(CreateImageRequest)
110 | if err := dec(in); err != nil {
111 | return nil, err
112 | }
113 | if interceptor == nil {
114 | return srv.(ImageStorageServer).CreateImage(ctx, in)
115 | }
116 | info := &grpc.UnaryServerInfo{
117 | Server: srv,
118 | FullMethod: ImageStorage_CreateImage_FullMethodName,
119 | }
120 | handler := func(ctx context.Context, req interface{}) (interface{}, error) {
121 | return srv.(ImageStorageServer).CreateImage(ctx, req.(*CreateImageRequest))
122 | }
123 | return interceptor(ctx, in, info, handler)
124 | }
125 |
126 | func _ImageStorage_GetImage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
127 | in := new(FindImageRequest)
128 | if err := dec(in); err != nil {
129 | return nil, err
130 | }
131 | if interceptor == nil {
132 | return srv.(ImageStorageServer).GetImage(ctx, in)
133 | }
134 | info := &grpc.UnaryServerInfo{
135 | Server: srv,
136 | FullMethod: ImageStorage_GetImage_FullMethodName,
137 | }
138 | handler := func(ctx context.Context, req interface{}) (interface{}, error) {
139 | return srv.(ImageStorageServer).GetImage(ctx, req.(*FindImageRequest))
140 | }
141 | return interceptor(ctx, in, info, handler)
142 | }
143 |
144 | func _ImageStorage_GetImages_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
145 | in := new(FindImagesRequest)
146 | if err := dec(in); err != nil {
147 | return nil, err
148 | }
149 | if interceptor == nil {
150 | return srv.(ImageStorageServer).GetImages(ctx, in)
151 | }
152 | info := &grpc.UnaryServerInfo{
153 | Server: srv,
154 | FullMethod: ImageStorage_GetImages_FullMethodName,
155 | }
156 | handler := func(ctx context.Context, req interface{}) (interface{}, error) {
157 | return srv.(ImageStorageServer).GetImages(ctx, req.(*FindImagesRequest))
158 | }
159 | return interceptor(ctx, in, info, handler)
160 | }
161 |
162 | // ImageStorage_ServiceDesc is the grpc.ServiceDesc for ImageStorage service.
163 | // It's only intended for direct use with grpc.RegisterService,
164 | // and not to be introspected or modified (even as a copy)
165 | var ImageStorage_ServiceDesc = grpc.ServiceDesc{
166 | ServiceName: "gateway.ImageStorage",
167 | HandlerType: (*ImageStorageServer)(nil),
168 | Methods: []grpc.MethodDesc{
169 | {
170 | MethodName: "CreateImage",
171 | Handler: _ImageStorage_CreateImage_Handler,
172 | },
173 | {
174 | MethodName: "GetImage",
175 | Handler: _ImageStorage_GetImage_Handler,
176 | },
177 | {
178 | MethodName: "GetImages",
179 | Handler: _ImageStorage_GetImages_Handler,
180 | },
181 | },
182 | Streams: []grpc.StreamDesc{},
183 | Metadata: "image_storage.proto",
184 | }
185 |
--------------------------------------------------------------------------------
/services/storage/prod.Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.19-alpine
2 |
3 | WORKDIR /usr/src/app
4 |
5 | ENV CGO_ENABLED=0
6 |
7 | COPY ./ ./
8 |
9 | RUN apk add --no-cache make && go mod download
10 |
11 | ENTRYPOINT go build -o ./.bin/app ./cmd/main.go && ./.bin/app
--------------------------------------------------------------------------------
/services/storage/transport/grpc/handler/handler.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "context"
5 | "google.golang.org/protobuf/types/known/emptypb"
6 |
7 | "github.com/rvinnie/lightstream/services/storage/aws"
8 | "github.com/rvinnie/lightstream/services/storage/config"
9 | pb "github.com/rvinnie/lightstream/services/storage/pb"
10 | )
11 |
12 | type ImageStorageService interface {
13 | CreateImage(ctx context.Context, request *pb.CreateImageRequest) (*emptypb.Empty, error)
14 | GetImage(ctx context.Context, request *pb.FindImageRequest) (*pb.FindImageResponse, error)
15 | GetImages(ctx context.Context, request *pb.FindImagesRequest) (*pb.FindImagesResponse, error)
16 | }
17 |
18 | type ImageStorageHandler struct {
19 | pb.UnimplementedImageStorageServer
20 |
21 | manager aws.AWS
22 | cfg config.AWSConfig
23 | }
24 |
25 | func NewImageStorageHandler(m aws.AWS, cfg config.AWSConfig) *ImageStorageHandler {
26 | return &ImageStorageHandler{manager: m, cfg: cfg}
27 | }
28 |
29 | func (h *ImageStorageHandler) CreateImage(ctx context.Context, request *pb.CreateImageRequest) (*emptypb.Empty, error) {
30 | err := h.manager.UploadObject(request.Path, request.ContentType, request.Image)
31 |
32 | return &emptypb.Empty{}, err
33 | }
34 |
35 | func (h *ImageStorageHandler) GetImage(ctx context.Context, request *pb.FindImageRequest) (*pb.FindImageResponse, error) {
36 | object, err := h.manager.DownloadObject(request.Path)
37 |
38 | imageResponse := &pb.FindImageResponse{
39 | Image: object.Body,
40 | ContentType: object.ContentType,
41 | Name: object.Name,
42 | }
43 |
44 | return imageResponse, err
45 | }
46 |
47 | func (h *ImageStorageHandler) GetImages(ctx context.Context, request *pb.FindImagesRequest) (*pb.FindImagesResponse, error) {
48 | objects, err := h.manager.DownloadObjects(request.Paths)
49 |
50 | var imagesResponses []*pb.FindImageResponse
51 | for _, object := range objects {
52 | imageResponse := &pb.FindImageResponse{
53 | Name: object.Name,
54 | ContentType: object.ContentType,
55 | Image: object.Body,
56 | }
57 | imagesResponses = append(imagesResponses, imageResponse)
58 | }
59 |
60 | return &pb.FindImagesResponse{Images: imagesResponses}, err
61 | }
62 |
--------------------------------------------------------------------------------
/services/storage/transport/grpc/server.go:
--------------------------------------------------------------------------------
1 | package grpc
2 |
3 | import (
4 | "fmt"
5 | "net"
6 |
7 | pb "github.com/rvinnie/lightstream/services/storage/pb"
8 | "google.golang.org/grpc"
9 | )
10 |
11 | type Server struct {
12 | imageStorageHandler pb.ImageStorageServer
13 | srv *grpc.Server
14 | }
15 |
16 | func NewServer(imageStorageHandler pb.ImageStorageServer) *Server {
17 | return &Server{
18 | srv: grpc.NewServer(),
19 | imageStorageHandler: imageStorageHandler,
20 | }
21 | }
22 |
23 | func (s *Server) ListenAndServe(port string) error {
24 | addr := fmt.Sprintf(":%s", port)
25 |
26 | lis, err := net.Listen("tcp", addr)
27 | if err != nil {
28 | return err
29 | }
30 |
31 | pb.RegisterImageStorageServer(s.srv, s.imageStorageHandler)
32 |
33 | if err = s.srv.Serve(lis); err != nil {
34 | return err
35 | }
36 |
37 | return nil
38 | }
39 |
40 | func (s *Server) Stop() {
41 | s.srv.GracefulStop()
42 | }
43 |
--------------------------------------------------------------------------------
/website/css/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-size: 1em;
3 | width: 100%;
4 | height: 100%;
5 | position: fixed;
6 | top: 0;
7 | left: 0;
8 | overflow: auto;
9 | }
10 |
11 | h2 {
12 | font-family: 'Plus Jakarta Sans', sans-serif;
13 | font-weight: bold;
14 | font-size: 2.5em;
15 | text-align: center;
16 | color: #3E54AC;
17 | margin-bottom: 1em;
18 | margin-top: 0;
19 | }
20 |
21 | input {
22 | font-family: 'Plus Jakarta Sans', sans-serif;
23 | font-weight: bold;
24 | margin: 0;
25 | padding: 0;
26 |
27 | }
28 |
29 | input[type="submit"] {
30 | font-family: 'Plus Jakarta Sans', sans-serif;
31 | font-weight: bold;
32 | margin: 0 0.1em;
33 | border-width: 0;
34 | background-color: #674188;
35 | }
36 |
37 | input[type="search"] {
38 | font-family: 'Plus Jakarta Sans', sans-serif;
39 | font-weight: bold;
40 | margin-right: 0.1em;
41 | }
42 |
43 | input[type="file"] {
44 | font-family: 'Plus Jakarta Sans', sans-serif;
45 | margin-right: 0.1em;
46 | background-color: #fff;
47 | color: #674188;
48 | }
49 |
50 | input[type=file]::file-selector-button {
51 | border-width: 0;
52 | height: 3.3em;
53 | padding: 0 16px;
54 | margin-right: 16px;
55 | background-color: #C3ACD0;
56 | color: #3E54AC;
57 | }
58 |
59 | .wrapper {
60 | position: absolute;
61 | top: 50%;
62 | left: 50%;
63 | transform: translate(-50%, -50%);
64 | display: grid;
65 | padding-bottom: 10em;
66 | }
67 |
68 | .search-form {
69 | display: grid;
70 | grid-template-columns: repeat(5, 1fr);
71 | margin-bottom: 0.5em;
72 | }
73 |
74 | .search-input {
75 | grid-column: 1 / 4;
76 | padding: 1em;
77 | }
78 |
79 | .upload-form {
80 | display: grid;
81 | cursor: pointer;
82 | align-items: center;
83 | grid-template-columns: repeat(5, 1fr);
84 | }
85 |
86 | .upload-file {
87 | grid-column: 1 / 5;
88 | cursor: pointer;
89 | }
90 |
91 | .notify {
92 | --notify-error: #eb5757;
93 | --notify-success: #6fcf97;
94 | --notify-warning: #f2c94c;
95 | --notify-gray: #333333;
96 | --notify-gray-2: #4d4d4d;
97 | --notify-gray-3: #828282;
98 | --notify-white: #fff;
99 | --notify-white-2: rgba(255, 255, 255, 0.8);
100 | --notify-padding: 0.75rem;
101 | --notify-icon-size: 32px;
102 | --notify-close-icon-size: 16px;
103 | }
--------------------------------------------------------------------------------
/website/images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rvinnie/lightstream/69dedca6bc746ef456b14462416ceedec4beb711/website/images/favicon.ico
--------------------------------------------------------------------------------
/website/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | Lightstream
15 |
16 |
17 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/website/js/script.js:
--------------------------------------------------------------------------------
1 | const searchBttn = document.getElementById("searchSubmit");
2 | const searchAllBttn = document.getElementById("searchAll");
3 | const searchElement = document.getElementById("searchInput");
4 | const uploadBttn = document.getElementById("uploadSubmit");
5 | const uploadFile = document.getElementById("uploadFile");
6 |
7 | const url = 'http://localhost:8080'
8 |
9 | // Notifications
10 | const NotifyStatuses = { SUCCESS: 'success', ERROR: 'error', WARNING: 'warning' };
11 |
12 | function pushNotify(status, title) {
13 | let myNotify = new Notify({
14 | status: status,
15 | title: title,
16 | effect: 'slide',
17 | autoclose: true,
18 | autotimeout: 3000,
19 | type: 3
20 | })
21 | }
22 |
23 | // Getting single image
24 | async function searchImage() {
25 | const searchId = searchElement.value
26 |
27 | if (searchId == "") {
28 | pushNotify(NotifyStatuses.ERROR, `Enter image id`)
29 | return
30 | }
31 |
32 | const uri = `${url}/images/${searchId}`
33 | const response = await fetch(uri, { method: "GET" })
34 |
35 | if (response.status !== 200) {
36 | pushNotify(NotifyStatuses.ERROR, `Image with such id does not exist `)
37 | return
38 | }
39 |
40 | const imageJson = await response.json()
41 |
42 |
43 | new Fancybox([
44 | {
45 | src: "data:" + imageJson.contentType + ";base64," + imageJson.data,
46 | type: "image",
47 | },
48 | ], {hideScrollbar: false});
49 | }
50 |
51 | // Getting all images
52 | async function searchImages() {
53 | const uri = `${url}/images`
54 | const response = await fetch(uri, { method: "GET" })
55 |
56 | if (response.status !== 200) {
57 | pushNotify(NotifyStatuses.ERROR, 'Unable to get images')
58 | return
59 | }
60 |
61 | const images = await response.json()
62 | let galleryItems = [];
63 |
64 | if (images.length == 0) {
65 | pushNotify(NotifyStatuses.WARNING, 'Storage is empty')
66 | return
67 | }
68 |
69 | for (let i = 0; i < images.length; i++) {
70 | let galleryItem = {
71 | src: "data:" + images[i].contentType + ";base64," + images[i].data,
72 | type: "image",
73 | }
74 | galleryItems.push(galleryItem)
75 | }
76 |
77 | new Fancybox(galleryItems, {hideScrollbar: false})
78 | }
79 |
80 |
81 | // Saving image script
82 | function saveImage() {
83 | const uri = `${url}/images/add`
84 | const file = uploadFile.files[0];
85 |
86 | if (file == null) {
87 | pushNotify(NotifyStatuses.ERROR, `Choose file`)
88 | return
89 | }
90 |
91 | let reader = new FileReader();
92 | reader.readAsArrayBuffer(file)
93 |
94 | reader.onload = async function () {
95 | const options = {
96 | method: 'POST',
97 | headers: {
98 | 'Content-Type': file.type,
99 | 'Filename': file.name,
100 | },
101 | body: reader.result
102 | };
103 |
104 | const response = await fetch(uri, options)
105 | const id = await response.json()
106 |
107 | if (response.status === 201) {
108 | pushNotify(NotifyStatuses.SUCCESS, `Image successful uploaded with id=${id}`)
109 | } else {
110 | pushNotify(NotifyStatuses.ERROR, `Unable to upload file`)
111 | }
112 | }
113 | }
114 |
115 | uploadBttn.onclick = saveImage;
116 | searchBttn.onclick = searchImage;
117 | searchAllBttn.onclick = searchImages;
118 |
--------------------------------------------------------------------------------