├── .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 | Static Badge 15 |

16 |
17 | 18 | 19 | 20 |
21 | Table of Contents 22 |
    23 |
  1. About The Project 24 | 29 |
  2. 30 |
  3. Getting Started 31 | 34 |
  4. 35 |
  5. Usage
  6. 36 |
  7. Roadmap
  8. 37 |
  9. Contributing
  10. 38 |
  11. Contact
  12. 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 | diagram 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 | grafana 75 | 76 | If the server crashes or the load is too high, the Alertmanager will send a notification in Telegram. 77 | 78 | 79 |

80 | grafana 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 | ci 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 | Index 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 | Gallery 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 |
18 |
19 |

Welcome to Lightstream

20 |
21 |
22 | 23 | 24 | 25 |
26 |
27 | 28 | 29 |
30 |
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 | --------------------------------------------------------------------------------