├── .env.example ├── .gitignore ├── Dockerfile ├── Dockerfile.prod ├── Makefile ├── README.md ├── application ├── repositories │ ├── job_repository.go │ ├── job_repository_test.go │ ├── video_repository.go │ └── video_repository_test.go └── services │ ├── job_manager.go │ ├── job_service.go │ ├── job_worker.go │ ├── upload_manager.go │ ├── upload_manager_test.go │ ├── video_service.go │ └── video_service_test.go ├── docker-compose.yml ├── domain ├── job.go ├── job_test.go ├── video.go └── video_test.go ├── framework ├── cmd │ └── server │ │ └── server.go ├── database │ └── db.go ├── queue │ └── queue.go └── utils │ ├── utils.go │ └── utils_test.go ├── go.mod └── go.sum /.env.example: -------------------------------------------------------------------------------- 1 | DB_TYPE="postgres" 2 | DSN="dbname=encoder sslmode=disable user=postgres password=root host=db" 3 | 4 | DB_TYPE_TEST="sqlite3" 5 | DSN_TEST=":memory:" 6 | 7 | ENV="dev" 8 | DEBUG=true 9 | AUTO_MIGRATE_DB=true 10 | 11 | localStoragePath="/tmp" 12 | inputBucketName="codeeducationtest" 13 | outputBucketName="codeeducationtest" 14 | CONCURRENCY_UPLOAD=50 15 | CONCURRENCY_WORKERS=2 16 | 17 | RABBITMQ_DEFAULT_USER=rabbitmq 18 | RABBITMQ_DEFAULT_PASS=rabbitmq 19 | RABBITMQ_DEFAULT_HOST=rabbit 20 | RABBITMQ_DEFAULT_PORT=5672 21 | RABBITMQ_DEFAULT_VHOST=/ 22 | RABBITMQ_CONSUMER_NAME=app-name 23 | RABBITMQ_CONSUMER_QUEUE_NAME=videos 24 | RABBITMQ_NOTIFICATION_EX=amq.direct 25 | RABBITMQ_NOTIFICATION_ROUTING_KEY=jobs 26 | RABBITMQ_DLX=dlx 27 | 28 | GOOGLE_APPLICATION_CREDENTIALS="/go/src/bucket-credential.json" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .env 3 | bucket-credential.json 4 | .pgdata 5 | .devcontainer -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.14.6-alpine3.11 2 | ENV PATH="$PATH:/bin/bash" \ 3 | BENTO4_BIN="/opt/bento4/bin" \ 4 | PATH="$PATH:/opt/bento4/bin" 5 | 6 | # FFMPEG 7 | RUN apk add --update ffmpeg bash make 8 | 9 | # Install Bento 10 | WORKDIR /tmp/bento4 11 | ENV BENTO4_BASE_URL="http://zebulon.bok.net/Bento4/source/" \ 12 | BENTO4_VERSION="1-5-0-615" \ 13 | BENTO4_CHECKSUM="5378dbb374343bc274981d6e2ef93bce0851bda1" \ 14 | BENTO4_TARGET="" \ 15 | BENTO4_PATH="/opt/bento4" \ 16 | BENTO4_TYPE="SRC" 17 | # download and unzip bento4 18 | RUN apk add --update --upgrade python unzip bash gcc g++ scons && \ 19 | wget -q ${BENTO4_BASE_URL}/Bento4-${BENTO4_TYPE}-${BENTO4_VERSION}${BENTO4_TARGET}.zip && \ 20 | sha1sum -b Bento4-${BENTO4_TYPE}-${BENTO4_VERSION}${BENTO4_TARGET}.zip | grep -o "^$BENTO4_CHECKSUM " && \ 21 | mkdir -p ${BENTO4_PATH} && \ 22 | unzip Bento4-${BENTO4_TYPE}-${BENTO4_VERSION}${BENTO4_TARGET}.zip -d ${BENTO4_PATH} && \ 23 | rm -rf Bento4-${BENTO4_TYPE}-${BENTO4_VERSION}${BENTO4_TARGET}.zip && \ 24 | apk del unzip && \ 25 | # don't do these steps if using binary install 26 | cd ${BENTO4_PATH} && scons -u build_config=Release target=x86_64-unknown-linux && \ 27 | cp -R ${BENTO4_PATH}/Build/Targets/x86_64-unknown-linux/Release ${BENTO4_PATH}/bin && \ 28 | cp -R ${BENTO4_PATH}/Source/Python/utils ${BENTO4_PATH}/utils && \ 29 | cp -a ${BENTO4_PATH}/Source/Python/wrappers/. ${BENTO4_PATH}/bin 30 | 31 | WORKDIR /go/src 32 | 33 | ENTRYPOINT ["tail", "-f", "/dev/null"] 34 | -------------------------------------------------------------------------------- /Dockerfile.prod: -------------------------------------------------------------------------------- 1 | FROM golang:1.13-alpine as build-env 2 | 3 | RUN apk add --update --upgrade build-base 4 | 5 | WORKDIR /go/src 6 | 7 | COPY . . 8 | RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" framework/cmd/server/server.go 9 | 10 | FROM alpine 11 | 12 | ENV PATH="$PATH:/bin/bash" \ 13 | BENTO4_BIN="/opt/bento4/bin" \ 14 | PATH="$PATH:/opt/bento4/bin" 15 | 16 | # FFMPEG 17 | RUN apk add --update ffmpeg bash curl 18 | 19 | # Install Bento 20 | WORKDIR /tmp/bento4 21 | ENV BENTO4_BASE_URL="http://zebulon.bok.net/Bento4/source/" \ 22 | BENTO4_VERSION="1-5-0-615" \ 23 | BENTO4_CHECKSUM="5378dbb374343bc274981d6e2ef93bce0851bda1" \ 24 | BENTO4_TARGET="" \ 25 | BENTO4_PATH="/opt/bento4" \ 26 | BENTO4_TYPE="SRC" 27 | # download and unzip bento4 28 | RUN apk add --update --upgrade curl python unzip bash gcc g++ scons && \ 29 | curl -O -s ${BENTO4_BASE_URL}/Bento4-${BENTO4_TYPE}-${BENTO4_VERSION}${BENTO4_TARGET}.zip && \ 30 | sha1sum -b Bento4-${BENTO4_TYPE}-${BENTO4_VERSION}${BENTO4_TARGET}.zip | grep -o "^$BENTO4_CHECKSUM " && \ 31 | mkdir -p ${BENTO4_PATH} && \ 32 | unzip Bento4-${BENTO4_TYPE}-${BENTO4_VERSION}${BENTO4_TARGET}.zip -d ${BENTO4_PATH} && \ 33 | rm -rf Bento4-${BENTO4_TYPE}-${BENTO4_VERSION}${BENTO4_TARGET}.zip && \ 34 | apk del unzip && \ 35 | # don't do these steps if using binary install 36 | cd ${BENTO4_PATH} && scons -u build_config=Release target=x86_64-unknown-linux && \ 37 | cp -R ${BENTO4_PATH}/Build/Targets/x86_64-unknown-linux/Release ${BENTO4_PATH}/bin && \ 38 | cp -R ${BENTO4_PATH}/Source/Python/utils ${BENTO4_PATH}/utils && \ 39 | cp -a ${BENTO4_PATH}/Source/Python/wrappers/. ${BENTO4_PATH}/bin 40 | 41 | WORKDIR /app 42 | 43 | COPY --from=build-env /go/src/.env /app 44 | COPY --from=build-env /go/src/bucket-credential.json /app 45 | COPY --from=build-env /go/src/server . 46 | RUN ls -lah 47 | 48 | ENTRYPOINT [ "./server" ] -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | server: 2 | go run framework/cmd/server/server.go 3 | 4 | test: 5 | go test -cover ./... 6 | 7 | .PHONY: server test -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Microsserviço de encoder de vídeo 2 | 3 | ## Configurando ambiente 4 | 5 | Para rodar em modo de desenvolvimento, siga os seguintes passos: 6 | 7 | * Duplique o arquivo `.env.example` para `.env` 8 | * Execute o docker-compose up -d 9 | * Acesse a administração do rabbitmq e crie uma exchange do tipo `fannout`. Ela será uma `Dead Letter Exchange` para receber as mensagens que não forem processadas. 10 | * Crie uma `Dead Letter Queue` e faça o binding da mesma na `Dead Letter Exchange` que acaba de ser criada. Não há necessidade de routing_key. 11 | * No arquivo `.env` informe o nome da `Dead Letter Exchange` no parâmetro: `RABBITMQ_DLX` 12 | * Crie uma conta de serviço no GCP que tenha permissão para gravar no google cloud storage. Baixe o arquivo json com as credenciais e salve-o na raiz do projeto exatamente com o nome: `bucket-credential.json` 13 | 14 | ## Executando 15 | 16 | Para executar o encoder rode o comando `make server` diretamente no container. Exemplo: 17 | 18 | ``` 19 | docker exec encoder-new2_app_1 make server 20 | ``` 21 | 22 | Sendo que `microsservico-enconder_app_1` é o nome nome do container gerado pelo docker-compose. 23 | 24 | ## Padrão de envio de mensagem para o encoder 25 | 26 | Para que uma mensagem possa ser parseada pelo sistema de encoder, ela deverá chegar no seguinte formato em json: 27 | 28 | ``` 29 | { 30 | "resource_id": "my-resource-id-can-be-a-uuid-type", 31 | "file_path": "convite.mp4" 32 | } 33 | ``` 34 | 35 | * `resource_id`: Representa o ID do vídeo que você deseja converter. Ele é do tipo string. 36 | * `file_path`: É o caminho completo do vídeo mp4 dentro do bucket. 37 | 38 | ## Padrão de retorno de mensagem pelo encoder 39 | 40 | ### Sucesso no processamento 41 | 42 | Para cada vídeo processado, o encoder enviará para uma exchange (a ser configurada no .env) o resultado do processamento. 43 | 44 | Caso o processamento tenha sido concluído com sucesso, o padrão de retorno em json será: 45 | 46 | ``` 47 | { 48 | "id":"bbbdd123-ad05-4dc8-a74c-d63a0a2423d5", 49 | "output_bucket_path":"codeeducationtest", 50 | "status":"COMPLETED", 51 | "video":{ 52 | "encoded_video_folder":"b3f2d41e-2c0a-4830-bd65-68227e97764f", 53 | "resource_id":"aadc5ff9-0b0d-13ab-4a40-a11b2eaa148c", 54 | "file_path":"convite.mp4" 55 | }, 56 | "Error":"", 57 | "created_at":"2020-05-27T19:43:34.850479-04:00", 58 | "updated_at":"2020-05-27T19:43:38.081754-04:00" 59 | } 60 | ``` 61 | 62 | Sendo que `encoded_video_folder` é a pasta que possui o vídeo convertido. 63 | 64 | ### Erro no processamento 65 | 66 | Caso o processamento tenha encontrado algum erro, o padrão de retorno em json será: 67 | 68 | ``` 69 | { 70 | "message": { 71 | "resource_id": "aadc5ff9-010d-a3ab-4a40-a11b2eaa148c", 72 | "file_path": "convite.mp4" 73 | }, 74 | "error":"Motivo do erro" 75 | } 76 | ``` 77 | 78 | Além disso, o encoder enviará para uma dead letter exchange a mensagem original que houve problema durante o processamento. 79 | Basta configurar a DLX desejada no arquivo .env no parâmetro: `RABBITMQ_DLX` -------------------------------------------------------------------------------- /application/repositories/job_repository.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import ( 4 | "encoder/domain" 5 | "fmt" 6 | "github.com/jinzhu/gorm" 7 | ) 8 | 9 | type JobRepository interface { 10 | Insert(job *domain.Job) (*domain.Job, error) 11 | Find(id string) (*domain.Job, error) 12 | Update(job *domain.Job) (*domain.Job, error) 13 | } 14 | 15 | type JobRepositoryDb struct { 16 | Db *gorm.DB 17 | } 18 | 19 | func (repo JobRepositoryDb) Insert(job *domain.Job) (*domain.Job, error) { 20 | 21 | err := repo.Db.Create(job).Error 22 | 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | return job, nil 28 | 29 | } 30 | 31 | func (repo JobRepositoryDb) Find(id string) (*domain.Job, error) { 32 | 33 | var job domain.Job 34 | repo.Db.Preload("Video").First(&job, "id = ?", id) 35 | 36 | if job.ID == "" { 37 | return nil, fmt.Errorf("job does not exist") 38 | } 39 | 40 | return &job, nil 41 | } 42 | 43 | func (repo JobRepositoryDb) Update(job *domain.Job) (*domain.Job, error) { 44 | err := repo.Db.Save(&job).Error 45 | 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | return job, nil 51 | } 52 | -------------------------------------------------------------------------------- /application/repositories/job_repository_test.go: -------------------------------------------------------------------------------- 1 | package repositories_test 2 | 3 | import ( 4 | "encoder/application/repositories" 5 | "encoder/domain" 6 | "encoder/framework/database" 7 | uuid "github.com/satori/go.uuid" 8 | "github.com/stretchr/testify/require" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | func TestJobRepositoryDbInsert(t *testing.T) { 14 | db := database.NewDbTest() 15 | defer db.Close() 16 | 17 | video := domain.NewVideo() 18 | video.ID = uuid.NewV4().String() 19 | video.FilePath = "path" 20 | video.CreatedAt = time.Now() 21 | 22 | repo := repositories.VideoRepositoryDb{Db: db} 23 | repo.Insert(video) 24 | 25 | job, err := domain.NewJob("output_path", "Pending", video) 26 | require.Nil(t, err) 27 | 28 | repoJob := repositories.JobRepositoryDb{Db: db} 29 | repoJob.Insert(job) 30 | 31 | j, err := repoJob.Find(job.ID) 32 | require.NotEmpty(t, j.ID) 33 | require.Nil(t, err) 34 | require.Equal(t, j.ID, job.ID) 35 | require.Equal(t, j.VideoID, video.ID) 36 | } 37 | 38 | func TestJobRepositoryDbUpdate(t *testing.T) { 39 | db := database.NewDbTest() 40 | defer db.Close() 41 | 42 | video := domain.NewVideo() 43 | video.ID = uuid.NewV4().String() 44 | video.FilePath = "path" 45 | video.CreatedAt = time.Now() 46 | 47 | repo := repositories.VideoRepositoryDb{Db: db} 48 | repo.Insert(video) 49 | 50 | job, err := domain.NewJob("output_path", "Pending", video) 51 | require.Nil(t, err) 52 | 53 | repoJob := repositories.JobRepositoryDb{Db: db} 54 | repoJob.Insert(job) 55 | 56 | job.Status = "Complete" 57 | 58 | repoJob.Update(job) 59 | 60 | j, err := repoJob.Find(job.ID) 61 | require.NotEmpty(t, j.ID) 62 | require.Nil(t, err) 63 | require.Equal(t, j.Status, job.Status) 64 | } 65 | -------------------------------------------------------------------------------- /application/repositories/video_repository.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import ( 4 | "encoder/domain" 5 | "fmt" 6 | "github.com/jinzhu/gorm" 7 | uuid "github.com/satori/go.uuid" 8 | ) 9 | 10 | type VideoRepository interface { 11 | Insert(video *domain.Video) (*domain.Video, error) 12 | Find(id string) (*domain.Video, error) 13 | } 14 | 15 | type VideoRepositoryDb struct { 16 | Db *gorm.DB 17 | } 18 | 19 | func NewVideoRepository(db *gorm.DB) *VideoRepositoryDb { 20 | return &VideoRepositoryDb{Db: db} 21 | } 22 | 23 | func (repo VideoRepositoryDb) Insert(video *domain.Video) (*domain.Video, error) { 24 | 25 | if video.ID == "" { 26 | video.ID = uuid.NewV4().String() 27 | } 28 | 29 | err := repo.Db.Create(video).Error 30 | 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | return video, nil 36 | 37 | } 38 | 39 | func (repo VideoRepositoryDb) Find(id string) (*domain.Video, error) { 40 | 41 | var video domain.Video 42 | repo.Db.Preload("Jobs").First(&video, "id = ?", id) 43 | 44 | if video.ID == "" { 45 | return nil, fmt.Errorf("video does not exist") 46 | } 47 | 48 | return &video, nil 49 | 50 | } 51 | -------------------------------------------------------------------------------- /application/repositories/video_repository_test.go: -------------------------------------------------------------------------------- 1 | package repositories_test 2 | 3 | import ( 4 | "encoder/application/repositories" 5 | "encoder/domain" 6 | "encoder/framework/database" 7 | uuid "github.com/satori/go.uuid" 8 | "github.com/stretchr/testify/require" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | func TestVideoRepositoryDbInsert(t *testing.T) { 14 | db := database.NewDbTest() 15 | defer db.Close() 16 | 17 | video := domain.NewVideo() 18 | video.ID = uuid.NewV4().String() 19 | video.FilePath = "path" 20 | video.CreatedAt = time.Now() 21 | 22 | repo := repositories.VideoRepositoryDb{Db:db} 23 | repo.Insert(video) 24 | 25 | v, err := repo.Find(video.ID) 26 | 27 | require.NotEmpty(t, v.ID) 28 | require.Nil(t, err) 29 | require.Equal(t, v.ID, video.ID) 30 | } 31 | -------------------------------------------------------------------------------- /application/services/job_manager.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "encoder/application/repositories" 5 | "encoder/domain" 6 | "encoder/framework/queue" 7 | "encoding/json" 8 | "github.com/jinzhu/gorm" 9 | "github.com/streadway/amqp" 10 | "log" 11 | "os" 12 | "strconv" 13 | ) 14 | 15 | type JobManager struct { 16 | Db *gorm.DB 17 | Domain domain.Job 18 | MessageChannel chan amqp.Delivery 19 | JobReturnChannel chan JobWorkerResult 20 | RabbitMQ *queue.RabbitMQ 21 | } 22 | 23 | type JobNotificationError struct { 24 | Message string `json:"message"` 25 | Error string `json:"error"` 26 | } 27 | 28 | func NewJobManager(db *gorm.DB, rabbitMQ *queue.RabbitMQ, jobReturnChannel chan JobWorkerResult, messageChannel chan amqp.Delivery) *JobManager { 29 | return &JobManager{ 30 | Db: db, 31 | Domain: domain.Job{}, 32 | MessageChannel: messageChannel, 33 | JobReturnChannel: jobReturnChannel, 34 | RabbitMQ: rabbitMQ, 35 | } 36 | } 37 | 38 | func (j *JobManager) Start(ch *amqp.Channel) { 39 | 40 | videoService := NewVideoService() 41 | videoService.VideoRepository = repositories.VideoRepositoryDb{Db: j.Db} 42 | 43 | jobService := JobService{ 44 | JobRepository: repositories.JobRepositoryDb{Db: j.Db}, 45 | VideoService: videoService, 46 | } 47 | 48 | concurrency, err := strconv.Atoi(os.Getenv("CONCURRENCY_WORKERS")) 49 | 50 | if err != nil { 51 | log.Fatalf("error loading var: CONCURRENCY_WORKERS.") 52 | } 53 | 54 | for qtdProcesses := 0; qtdProcesses < concurrency; qtdProcesses++ { 55 | go JobWorker(j.MessageChannel, j.JobReturnChannel, jobService, j.Domain, qtdProcesses) 56 | } 57 | 58 | for jobResult := range j.JobReturnChannel { 59 | if jobResult.Error != nil { 60 | err = j.checkParseErrors(jobResult) 61 | } else { 62 | err = j.notifySuccess(jobResult, ch) 63 | } 64 | 65 | if err != nil { 66 | jobResult.Message.Reject(false) 67 | } 68 | } 69 | } 70 | 71 | func (j *JobManager) notifySuccess(jobResult JobWorkerResult, ch *amqp.Channel) error { 72 | 73 | Mutex.Lock() 74 | jobJson, err := json.Marshal(jobResult.Job) 75 | Mutex.Unlock() 76 | 77 | if err != nil { 78 | return err 79 | } 80 | 81 | err = j.notify(jobJson) 82 | 83 | if err != nil { 84 | return err 85 | } 86 | 87 | err = jobResult.Message.Ack(false) 88 | 89 | if err != nil { 90 | return err 91 | } 92 | 93 | return nil 94 | } 95 | 96 | func (j *JobManager) checkParseErrors(jobResult JobWorkerResult) error { 97 | if jobResult.Job.ID != "" { 98 | log.Printf("MessageID: %v. Error during the job: %v with video: %v. Error: %v", 99 | jobResult.Message.DeliveryTag, jobResult.Job.ID, jobResult.Job.Video.ID, jobResult.Error.Error()) 100 | } else { 101 | log.Printf("MessageID: %v. Error parsing message: %v", jobResult.Message.DeliveryTag, jobResult.Error) 102 | } 103 | 104 | errorMsg := JobNotificationError{ 105 | Message: string(jobResult.Message.Body), 106 | Error: jobResult.Error.Error(), 107 | } 108 | 109 | jobJson, err := json.Marshal(errorMsg) 110 | 111 | err = j.notify(jobJson) 112 | 113 | if err != nil { 114 | return err 115 | } 116 | 117 | err = jobResult.Message.Reject(false) 118 | 119 | if err != nil { 120 | return err 121 | } 122 | 123 | return nil 124 | } 125 | 126 | func (j *JobManager) notify(jobJson []byte) error { 127 | 128 | err := j.RabbitMQ.Notify( 129 | string(jobJson), 130 | "application/json", 131 | os.Getenv("RABBITMQ_NOTIFICATION_EX"), 132 | os.Getenv("RABBITMQ_NOTIFICATION_ROUTING_KEY"), 133 | ) 134 | 135 | if err != nil { 136 | return err 137 | } 138 | 139 | return nil 140 | } 141 | -------------------------------------------------------------------------------- /application/services/job_service.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "encoder/application/repositories" 5 | "encoder/domain" 6 | "errors" 7 | "os" 8 | "strconv" 9 | ) 10 | 11 | type JobService struct { 12 | Job *domain.Job 13 | JobRepository repositories.JobRepository 14 | VideoService VideoService 15 | } 16 | 17 | func (j *JobService) Start() error { 18 | 19 | err := j.changeJobStatus("DOWNLOADING") 20 | 21 | if err != nil { 22 | return j.failJob(err) 23 | } 24 | 25 | err = j.VideoService.Download(os.Getenv("inputBucketName")) 26 | 27 | if err != nil { 28 | return j.failJob(err) 29 | } 30 | 31 | err = j.changeJobStatus("FRAGMENTING") 32 | 33 | if err != nil { 34 | return j.failJob(err) 35 | } 36 | 37 | err = j.VideoService.Fragment() 38 | 39 | if err != nil { 40 | return j.failJob(err) 41 | } 42 | 43 | err = j.changeJobStatus("ENCODING") 44 | 45 | if err != nil { 46 | return j.failJob(err) 47 | } 48 | 49 | err = j.VideoService.Encode() 50 | 51 | if err != nil { 52 | return j.failJob(err) 53 | } 54 | 55 | err = j.performUpload() 56 | 57 | if err != nil { 58 | return j.failJob(err) 59 | } 60 | 61 | err = j.changeJobStatus("FINISHING") 62 | 63 | if err != nil { 64 | return j.failJob(err) 65 | } 66 | 67 | err = j.VideoService.Finish() 68 | 69 | if err != nil { 70 | return j.failJob(err) 71 | } 72 | 73 | err = j.changeJobStatus("COMPLETED") 74 | 75 | if err != nil { 76 | return j.failJob(err) 77 | } 78 | 79 | return nil 80 | } 81 | 82 | func (j *JobService) performUpload() error { 83 | 84 | err := j.changeJobStatus("UPLOADING") 85 | 86 | if err != nil { 87 | return j.failJob(err) 88 | } 89 | 90 | videoUpload := NewVideoUpload() 91 | videoUpload.OutputBucket = os.Getenv("outputBucketName") 92 | videoUpload.VideoPath = os.Getenv("localStoragePath") + "/" + j.VideoService.Video.ID 93 | concurrency, _ := strconv.Atoi(os.Getenv("CONCURRENCY_UPLOAD")) 94 | doneUpload := make(chan string) 95 | 96 | go videoUpload.ProcessUpload(concurrency, doneUpload) 97 | 98 | var uploadResult string 99 | uploadResult = <-doneUpload 100 | 101 | if uploadResult != "upload completed" { 102 | return j.failJob(errors.New(uploadResult)) 103 | } 104 | 105 | return err 106 | } 107 | 108 | func (j *JobService) changeJobStatus(status string) error { 109 | var err error 110 | 111 | j.Job.Status = status 112 | j.Job, err = j.JobRepository.Update(j.Job) 113 | 114 | if err != nil { 115 | return j.failJob(err) 116 | } 117 | 118 | return nil 119 | } 120 | 121 | func (j *JobService) failJob(error error) error { 122 | 123 | j.Job.Status = "FAILED" 124 | j.Job.Error = error.Error() 125 | 126 | _, err := j.JobRepository.Update(j.Job) 127 | 128 | if err != nil { 129 | return err 130 | } 131 | 132 | return error 133 | } 134 | -------------------------------------------------------------------------------- /application/services/job_worker.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "encoder/domain" 5 | "encoder/framework/utils" 6 | "encoding/json" 7 | uuid "github.com/satori/go.uuid" 8 | "github.com/streadway/amqp" 9 | "os" 10 | "sync" 11 | "time" 12 | ) 13 | 14 | type JobWorkerResult struct { 15 | Job domain.Job 16 | Message *amqp.Delivery 17 | Error error 18 | } 19 | 20 | var Mutex = &sync.Mutex{} 21 | 22 | func JobWorker(messageChannel chan amqp.Delivery, returnChan chan JobWorkerResult, jobService JobService, job domain.Job, workerID int) { 23 | 24 | //{ 25 | // "resource_id":"id do video da pessoa que enviou para nossa fila", 26 | // "file_path": "convite.mp4" 27 | //} 28 | 29 | for message := range messageChannel { 30 | 31 | err := utils.IsJson(string(message.Body)) 32 | 33 | if err != nil { 34 | returnChan <- returnJobResult(domain.Job{}, message, err) 35 | continue 36 | } 37 | 38 | Mutex.Lock() 39 | err = json.Unmarshal(message.Body, &jobService.VideoService.Video) 40 | jobService.VideoService.Video.ID = uuid.NewV4().String() 41 | Mutex.Unlock() 42 | 43 | if err != nil { 44 | returnChan <- returnJobResult(domain.Job{}, message, err) 45 | continue 46 | } 47 | 48 | err = jobService.VideoService.Video.Validate() 49 | if err != nil { 50 | returnChan <- returnJobResult(domain.Job{}, message, err) 51 | continue 52 | } 53 | 54 | Mutex.Lock() 55 | err = jobService.VideoService.InsertVideo() 56 | Mutex.Unlock() 57 | if err != nil { 58 | returnChan <- returnJobResult(domain.Job{}, message, err) 59 | continue 60 | } 61 | 62 | job.Video = jobService.VideoService.Video 63 | job.OutputBucketPath = os.Getenv("outputBucketName") 64 | job.ID = uuid.NewV4().String() 65 | job.Status = "STARTING" 66 | job.CreatedAt = time.Now() 67 | 68 | Mutex.Lock() 69 | _, err = jobService.JobRepository.Insert(&job) 70 | Mutex.Unlock() 71 | 72 | if err != nil { 73 | returnChan <- returnJobResult(domain.Job{}, message, err) 74 | continue 75 | } 76 | 77 | jobService.Job = &job 78 | err = jobService.Start() 79 | 80 | if err != nil { 81 | returnChan <- returnJobResult(domain.Job{}, message, err) 82 | continue 83 | } 84 | 85 | returnChan <- returnJobResult(job, message, nil) 86 | 87 | } 88 | 89 | } 90 | func returnJobResult(job domain.Job, message amqp.Delivery, err error) JobWorkerResult { 91 | result := JobWorkerResult{ 92 | Job: job, 93 | Message: &message, 94 | Error: err, 95 | } 96 | return result 97 | } 98 | -------------------------------------------------------------------------------- /application/services/upload_manager.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "log" 7 | "os" 8 | "path/filepath" 9 | "runtime" 10 | "strings" 11 | 12 | "cloud.google.com/go/storage" 13 | ) 14 | 15 | type VideoUpload struct { 16 | Paths []string 17 | VideoPath string 18 | OutputBucket string 19 | Errors []string 20 | } 21 | 22 | func NewVideoUpload() *VideoUpload { 23 | return &VideoUpload{} 24 | } 25 | 26 | func (vu *VideoUpload) UploadObject(objectPath string, client *storage.Client, ctx context.Context) error { 27 | 28 | // caminho/x/b/arquivo.mp4 29 | // split: caminho/x/b/ 30 | // [0] caminho/x/b/ 31 | // [1] arquivo.mp4 32 | path := strings.Split(objectPath, os.Getenv("localStoragePath")+"/") 33 | 34 | f, err := os.Open(objectPath) 35 | if err != nil { 36 | return err 37 | } 38 | 39 | defer f.Close() 40 | 41 | wc := client.Bucket(vu.OutputBucket).Object(path[1]).NewWriter(ctx) 42 | wc.ACL = []storage.ACLRule{{Entity: storage.AllUsers, Role: storage.RoleReader}} 43 | 44 | if _, err = io.Copy(wc, f); err != nil { 45 | return err 46 | } 47 | 48 | if err := wc.Close(); err != nil { 49 | return err 50 | } 51 | 52 | return nil 53 | 54 | } 55 | 56 | func (vu *VideoUpload) loadPaths() error { 57 | 58 | err := filepath.Walk(vu.VideoPath, func(path string, info os.FileInfo, err error) error { 59 | 60 | if !info.IsDir() { 61 | vu.Paths = append(vu.Paths, path) 62 | } 63 | return nil 64 | }) 65 | 66 | if err != nil { 67 | return err 68 | } 69 | return nil 70 | } 71 | 72 | func (vu *VideoUpload) ProcessUpload(concurrency int, doneUpload chan string) error { 73 | 74 | in := make(chan int, runtime.NumCPU()) // qual o arquivo baseado na posicao do slice Paths 75 | returnChannel := make(chan string) 76 | 77 | err := vu.loadPaths() 78 | if err != nil { 79 | return err 80 | } 81 | 82 | uploadClient, ctx, err := getClientUpload() 83 | if err != nil { 84 | return err 85 | } 86 | 87 | for process := 0; process < concurrency; process++ { 88 | go vu.uploadWorker(in, returnChannel, uploadClient, ctx) 89 | } 90 | 91 | go func() { 92 | for x := 0; x < len(vu.Paths); x++ { 93 | in <- x 94 | } 95 | }() 96 | 97 | countDoneWorker := 0 98 | for r := range returnChannel { 99 | countDoneWorker++ 100 | 101 | if r != "" { 102 | doneUpload <- r 103 | break 104 | } 105 | 106 | if countDoneWorker == len(vu.Paths) { 107 | close(in) 108 | } 109 | } 110 | 111 | return nil 112 | } 113 | 114 | func (vu *VideoUpload) uploadWorker(in chan int, returnChan chan string, uploadClient *storage.Client, ctx context.Context) { 115 | 116 | for x := range in { 117 | err := vu.UploadObject(vu.Paths[x], uploadClient, ctx) 118 | 119 | if err != nil { 120 | vu.Errors = append(vu.Errors, vu.Paths[x]) 121 | log.Printf("error during the upload: %v. Error: %v", vu.Paths[x], err) 122 | returnChan <- err.Error() 123 | } 124 | 125 | returnChan <- "" 126 | } 127 | 128 | returnChan <- "upload completed" 129 | } 130 | 131 | func getClientUpload() (*storage.Client, context.Context, error) { 132 | ctx := context.Background() 133 | 134 | client, err := storage.NewClient(ctx) 135 | if err != nil { 136 | return nil, nil, err 137 | } 138 | return client, ctx, nil 139 | } 140 | -------------------------------------------------------------------------------- /application/services/upload_manager_test.go: -------------------------------------------------------------------------------- 1 | package services_test 2 | 3 | import ( 4 | "encoder/application/services" 5 | "github.com/joho/godotenv" 6 | "github.com/stretchr/testify/require" 7 | "log" 8 | "os" 9 | "testing" 10 | ) 11 | 12 | func init() { 13 | err := godotenv.Load("../../.env") 14 | if err != nil { 15 | log.Fatalf("Error loading .env file") 16 | } 17 | } 18 | 19 | func TestVideoServiceUpload(t *testing.T) { 20 | 21 | video, repo := prepare() 22 | 23 | videoService := services.NewVideoService() 24 | videoService.Video = video 25 | videoService.VideoRepository = repo 26 | 27 | err := videoService.Download("codeeducationtest") 28 | require.Nil(t, err) 29 | 30 | err = videoService.Fragment() 31 | require.Nil(t, err) 32 | 33 | err = videoService.Encode() 34 | require.Nil(t, err) 35 | 36 | videoUpload := services.NewVideoUpload() 37 | videoUpload.OutputBucket = "codeeducationtest" 38 | videoUpload.VideoPath = os.Getenv("localStoragePath") + "/" + video.ID 39 | 40 | doneUpload := make(chan string) 41 | go videoUpload.ProcessUpload(50, doneUpload) 42 | 43 | result := <-doneUpload 44 | require.Equal(t, result, "upload completed") 45 | 46 | err = videoService.Finish() 47 | require.Nil(t, err) 48 | 49 | } 50 | -------------------------------------------------------------------------------- /application/services/video_service.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "cloud.google.com/go/storage" 5 | "context" 6 | "encoder/application/repositories" 7 | "encoder/domain" 8 | "io/ioutil" 9 | "log" 10 | "os" 11 | "os/exec" 12 | ) 13 | 14 | type VideoService struct { 15 | Video *domain.Video 16 | VideoRepository repositories.VideoRepository 17 | } 18 | 19 | func NewVideoService() VideoService { 20 | return VideoService{} 21 | } 22 | 23 | func (v *VideoService) Download(bucketName string) error { 24 | 25 | ctx := context.Background() 26 | 27 | client, err := storage.NewClient(ctx) 28 | if err != nil { 29 | return err 30 | } 31 | 32 | bkt := client.Bucket(bucketName) 33 | obj := bkt.Object(v.Video.FilePath) 34 | 35 | r, err := obj.NewReader(ctx) 36 | if err != nil { 37 | return err 38 | } 39 | defer r.Close() 40 | 41 | body, err := ioutil.ReadAll(r) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | f, err := os.Create(os.Getenv("localStoragePath") + "/" + v.Video.ID + ".mp4") 47 | if err != nil { 48 | return err 49 | } 50 | 51 | _, err = f.Write(body) 52 | if err != nil { 53 | return err 54 | } 55 | 56 | defer f.Close() 57 | 58 | log.Printf("video %v has been stored", v.Video.ID) 59 | 60 | return nil 61 | } 62 | 63 | func (v *VideoService) Fragment() error { 64 | 65 | err := os.Mkdir(os.Getenv("localStoragePath")+"/"+v.Video.ID, os.ModePerm) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | source := os.Getenv("localStoragePath") + "/" + v.Video.ID + ".mp4" 71 | target := os.Getenv("localStoragePath") + "/" + v.Video.ID + ".frag" 72 | 73 | cmd := exec.Command("mp4fragment", source, target) 74 | output, err := cmd.CombinedOutput() 75 | if err != nil { 76 | return err 77 | } 78 | 79 | printOutput(output) 80 | 81 | return nil 82 | } 83 | 84 | func (v *VideoService) Encode() error { 85 | cmdArgs := []string{} 86 | cmdArgs = append(cmdArgs, os.Getenv("localStoragePath")+"/"+v.Video.ID+".frag") 87 | cmdArgs = append(cmdArgs, "--use-segment-timeline") 88 | cmdArgs = append(cmdArgs, "-o") 89 | cmdArgs = append(cmdArgs, os.Getenv("localStoragePath")+"/"+v.Video.ID) 90 | cmdArgs = append(cmdArgs, "-f") 91 | cmdArgs = append(cmdArgs, "--exec-dir") 92 | cmdArgs = append(cmdArgs, "/opt/bento4/bin/") 93 | cmd := exec.Command("mp4dash", cmdArgs...) 94 | 95 | output, err := cmd.CombinedOutput() 96 | 97 | if err != nil { 98 | return err 99 | } 100 | 101 | printOutput(output) 102 | 103 | return nil 104 | } 105 | 106 | func (v *VideoService) Finish() error { 107 | 108 | err := os.Remove(os.Getenv("localStoragePath") + "/" + v.Video.ID + ".mp4") 109 | if err != nil { 110 | log.Println("error removing mp4 ", v.Video.ID, ".mp4") 111 | return err 112 | } 113 | 114 | err = os.Remove(os.Getenv("localStoragePath") + "/" + v.Video.ID + ".frag") 115 | if err != nil { 116 | log.Println("error removing frag ", v.Video.ID, ".frag") 117 | return err 118 | } 119 | 120 | err = os.RemoveAll(os.Getenv("localStoragePath") + "/" + v.Video.ID) 121 | if err != nil { 122 | log.Println("error removing mp4 ", v.Video.ID, ".mp4") 123 | return err 124 | } 125 | 126 | log.Println("files have been removed: ", v.Video.ID) 127 | 128 | return nil 129 | 130 | } 131 | 132 | func (v *VideoService) InsertVideo() error { 133 | _, err := v.VideoRepository.Insert(v.Video) 134 | 135 | if err != nil { 136 | return err 137 | } 138 | 139 | return nil 140 | } 141 | 142 | func printOutput(out []byte) { 143 | if len(out) > 0 { 144 | log.Printf("=====> Output: %s\n", string(out)) 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /application/services/video_service_test.go: -------------------------------------------------------------------------------- 1 | package services_test 2 | 3 | import ( 4 | "encoder/application/repositories" 5 | "encoder/application/services" 6 | "encoder/domain" 7 | "encoder/framework/database" 8 | uuid "github.com/satori/go.uuid" 9 | "github.com/stretchr/testify/require" 10 | "log" 11 | "testing" 12 | "time" 13 | "github.com/joho/godotenv" 14 | ) 15 | 16 | func init() { 17 | err := godotenv.Load("../../.env") 18 | if err != nil { 19 | log.Fatalf("Error loading .env file") 20 | } 21 | } 22 | 23 | func prepare() (*domain.Video, repositories.VideoRepositoryDb) { 24 | db := database.NewDbTest() 25 | defer db.Close() 26 | 27 | video := domain.NewVideo() 28 | video.ID = uuid.NewV4().String() 29 | video.FilePath = "convite.mp4" 30 | video.CreatedAt = time.Now() 31 | 32 | repo := repositories.VideoRepositoryDb{Db: db} 33 | 34 | return video, repo 35 | } 36 | 37 | func TestVideoServiceDownload(t *testing.T) { 38 | 39 | video, repo := prepare() 40 | 41 | videoService := services.NewVideoService() 42 | videoService.Video = video 43 | videoService.VideoRepository = repo 44 | 45 | err := videoService.Download("codeeducationtest") 46 | require.Nil(t, err) 47 | 48 | err = videoService.Fragment() 49 | require.Nil(t, err) 50 | 51 | err = videoService.Encode() 52 | require.Nil(t, err) 53 | 54 | err = videoService.Finish() 55 | require.Nil(t, err) 56 | } 57 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | 5 | app: 6 | build: . 7 | volumes: 8 | - .:/go/src/ 9 | 10 | db: 11 | image: postgres:9.4 12 | restart: always 13 | tty: true 14 | volumes: 15 | - .pgdata:/var/lib/postgresql/data 16 | environment: 17 | - POSTGRES_PASSWORD=root 18 | - POSTGRES_DB=encoder 19 | ports: 20 | - "5432:5432" 21 | 22 | rabbit: 23 | image: "rabbitmq:3-management" 24 | environment: 25 | RABBITMQ_ERLANG_COOKIE: "SWQOKODSQALRPCLNMEQG" 26 | RABBITMQ_DEFAULT_USER: "rabbitmq" 27 | RABBITMQ_DEFAULT_PASS: "rabbitmq" 28 | RABBITMQ_DEFAULT_VHOST: "/" 29 | ports: 30 | - "15672:15672" 31 | - "5672:5672" -------------------------------------------------------------------------------- /domain/job.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "github.com/asaskevich/govalidator" 5 | uuid "github.com/satori/go.uuid" 6 | "time" 7 | ) 8 | 9 | func init() { 10 | govalidator.SetFieldsRequiredByDefault(true) 11 | } 12 | 13 | type Job struct { 14 | ID string `json:"job_id" valid:"uuid" gorm:"type:uuid;primary_key"` 15 | OutputBucketPath string `json:"output_bucket_path" valid:"notnull"` 16 | Status string `json:"status" valid:"notnull"` 17 | Video *Video `json:"video" valid:"-"` 18 | VideoID string `json:"-" valid:"-" gorm:"column:video_id;type:uuid;notnull"` 19 | Error string `valid:"-"` 20 | CreatedAt time.Time `json:"created_at" valid:"-"` 21 | UpdatedAt time.Time `json:"updated_at" valid:"-"` 22 | } 23 | 24 | func NewJob(output string, status string, video *Video) (*Job, error) { 25 | 26 | job := Job{ 27 | OutputBucketPath: output, 28 | Status: status, 29 | Video: video, 30 | } 31 | 32 | job.prepare() 33 | 34 | err := job.Validate() 35 | 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | return &job, nil 41 | 42 | } 43 | 44 | func (job *Job) prepare() { 45 | 46 | job.ID = uuid.NewV4().String() 47 | job.CreatedAt = time.Now() 48 | job.UpdatedAt = time.Now() 49 | 50 | } 51 | 52 | func (job *Job) Validate() error { 53 | _, err := govalidator.ValidateStruct(job) 54 | 55 | if err != nil { 56 | return err 57 | } 58 | 59 | return nil 60 | } 61 | -------------------------------------------------------------------------------- /domain/job_test.go: -------------------------------------------------------------------------------- 1 | package domain_test 2 | 3 | import ( 4 | "encoder/domain" 5 | uuid "github.com/satori/go.uuid" 6 | "github.com/stretchr/testify/require" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestNewJob(t *testing.T) { 12 | video := domain.NewVideo() 13 | video.ID = uuid.NewV4().String() 14 | video.FilePath = "path" 15 | video.CreatedAt = time.Now() 16 | 17 | job, err := domain.NewJob("path", "Converted", video) 18 | require.NotNil(t, job) 19 | require.Nil(t, err) 20 | } 21 | -------------------------------------------------------------------------------- /domain/video.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "github.com/asaskevich/govalidator" 5 | "time" 6 | ) 7 | 8 | type Video struct { 9 | ID string `json:"encoded_video_folder" valid:"uuid" gorm:"type:uuid;primary_key"` 10 | ResourceID string `json:"resource_id" valid:"notnull" gorm:"type:varchar(255)"` 11 | FilePath string `json:"file_path" valid:"notnull" gorm:"type:varchar(255)"` 12 | CreatedAt time.Time `json:"-" valid:"-"` 13 | Jobs []*Job `json:"-" valid:"-" gorm:"ForeignKey:VideoID"` 14 | } 15 | 16 | func init() { 17 | govalidator.SetFieldsRequiredByDefault(true) 18 | } 19 | 20 | func NewVideo() *Video { 21 | return &Video{} 22 | } 23 | 24 | func (video *Video) Validate() error { 25 | 26 | _, err := govalidator.ValidateStruct(video) 27 | 28 | if err != nil { 29 | return err 30 | } 31 | 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /domain/video_test.go: -------------------------------------------------------------------------------- 1 | package domain_test 2 | 3 | import ( 4 | "encoder/domain" 5 | uuid "github.com/satori/go.uuid" 6 | "github.com/stretchr/testify/require" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestValidateIfVideoIsEmpty(t *testing.T) { 12 | video := domain.NewVideo() 13 | err := video.Validate() 14 | 15 | require.Error(t, err) 16 | } 17 | 18 | func TestVideoIdIsNotAUuid(t *testing.T) { 19 | video := domain.NewVideo() 20 | 21 | video.ID = "abc" 22 | video.ResourceID = "a" 23 | video.FilePath = "path" 24 | video.CreatedAt = time.Now() 25 | 26 | err := video.Validate() 27 | require.Error(t, err) 28 | } 29 | 30 | func TestVideoValidation(t *testing.T) { 31 | video := domain.NewVideo() 32 | 33 | video.ID = uuid.NewV4().String() 34 | video.ResourceID = "a" 35 | video.FilePath = "path" 36 | video.CreatedAt = time.Now() 37 | 38 | err := video.Validate() 39 | require.Nil(t, err) 40 | } 41 | -------------------------------------------------------------------------------- /framework/cmd/server/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoder/application/services" 5 | "encoder/framework/database" 6 | "encoder/framework/queue" 7 | "github.com/joho/godotenv" 8 | "github.com/streadway/amqp" 9 | "log" 10 | "os" 11 | "strconv" 12 | ) 13 | 14 | var db database.Database 15 | 16 | func init() { 17 | err := godotenv.Load() 18 | if err != nil { 19 | log.Fatalf("Error loading .env file") 20 | } 21 | 22 | autoMigrateDb, err := strconv.ParseBool(os.Getenv("AUTO_MIGRATE_DB")) 23 | if err != nil { 24 | log.Fatalf("Error parsing boolean env var") 25 | } 26 | 27 | debug, err := strconv.ParseBool(os.Getenv("DEBUG")) 28 | if err != nil { 29 | log.Fatalf("Error parsing boolean env var") 30 | } 31 | 32 | db.AutoMigrateDb = autoMigrateDb 33 | db.Debug = debug 34 | db.DsnTest = os.Getenv("DSN_TEST") 35 | db.Dsn = os.Getenv("DSN") 36 | db.DbTypeTest = os.Getenv("DB_TYPE_TEST") 37 | db.DbType = os.Getenv("DB_TYPE") 38 | db.Env = os.Getenv("ENV") 39 | } 40 | 41 | func main() { 42 | 43 | messageChannel := make(chan amqp.Delivery) 44 | jobReturnChannel := make(chan services.JobWorkerResult) 45 | 46 | dbConnection, err := db.Connect() 47 | 48 | if err != nil { 49 | log.Fatalf("error connecting to DB") 50 | } 51 | 52 | defer dbConnection.Close() 53 | 54 | rabbitMQ := queue.NewRabbitMQ() 55 | ch := rabbitMQ.Connect() 56 | defer ch.Close() 57 | 58 | rabbitMQ.Consume(messageChannel) 59 | 60 | jobManager := services.NewJobManager(dbConnection, rabbitMQ, jobReturnChannel, messageChannel) 61 | jobManager.Start(ch) 62 | 63 | } 64 | -------------------------------------------------------------------------------- /framework/database/db.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "encoder/domain" 5 | "github.com/jinzhu/gorm" 6 | _ "github.com/jinzhu/gorm/dialects/sqlite" 7 | _ "github.com/lib/pq" 8 | "log" 9 | ) 10 | 11 | type Database struct { 12 | Db *gorm.DB 13 | Dsn string 14 | DsnTest string 15 | DbType string 16 | DbTypeTest string 17 | Debug bool 18 | AutoMigrateDb bool 19 | Env string 20 | } 21 | 22 | func NewDb() *Database { 23 | return &Database{} 24 | } 25 | 26 | func NewDbTest() *gorm.DB { 27 | dbInstance := NewDb() 28 | dbInstance.Env = "test" 29 | dbInstance.DbTypeTest = "sqlite3" 30 | dbInstance.DsnTest = ":memory:" 31 | dbInstance.AutoMigrateDb = true 32 | dbInstance.Debug = true 33 | 34 | connection, err := dbInstance.Connect() 35 | 36 | if err != nil { 37 | log.Fatalf("Test db error: %v", err) 38 | } 39 | 40 | return connection 41 | } 42 | 43 | func (d *Database) Connect() (*gorm.DB, error) { 44 | 45 | var err error 46 | 47 | if d.Env != "test" { 48 | d.Db, err = gorm.Open(d.DbType, d.Dsn) 49 | } else { 50 | d.Db, err = gorm.Open(d.DbTypeTest, d.DsnTest) 51 | } 52 | 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | if d.Debug { 58 | d.Db.LogMode(true) 59 | } 60 | 61 | if d.AutoMigrateDb { 62 | d.Db.AutoMigrate(&domain.Video{}, &domain.Job{}, ) 63 | d.Db.Model(domain.Job{}).AddForeignKey("video_id", "videos (id)", "CASCADE", "CASCADE") 64 | } 65 | 66 | return d.Db, nil 67 | 68 | } 69 | -------------------------------------------------------------------------------- /framework/queue/queue.go: -------------------------------------------------------------------------------- 1 | package queue 2 | 3 | import ( 4 | "github.com/streadway/amqp" 5 | "log" 6 | "os" 7 | ) 8 | 9 | type RabbitMQ struct { 10 | User string 11 | Password string 12 | Host string 13 | Port string 14 | Vhost string 15 | ConsumerQueueName string 16 | ConsumerName string 17 | AutoAck bool 18 | Args amqp.Table 19 | Channel *amqp.Channel 20 | } 21 | 22 | func NewRabbitMQ() *RabbitMQ { 23 | 24 | rabbitMQArgs := amqp.Table{} 25 | rabbitMQArgs["x-dead-letter-exchange"] = os.Getenv("RABBITMQ_DLX") 26 | 27 | rabbitMQ := RabbitMQ{ 28 | User: os.Getenv("RABBITMQ_DEFAULT_USER"), 29 | Password: os.Getenv("RABBITMQ_DEFAULT_PASS"), 30 | Host: os.Getenv("RABBITMQ_DEFAULT_HOST"), 31 | Port: os.Getenv("RABBITMQ_DEFAULT_PORT"), 32 | Vhost: os.Getenv("RABBITMQ_DEFAULT_VHOST"), 33 | ConsumerQueueName: os.Getenv("RABBITMQ_CONSUMER_QUEUE_NAME"), 34 | ConsumerName: os.Getenv("RABBITMQ_CONSUMER_NAME"), 35 | AutoAck: false, 36 | Args: rabbitMQArgs, 37 | } 38 | 39 | return &rabbitMQ 40 | } 41 | 42 | func (r *RabbitMQ) Connect() *amqp.Channel { 43 | dsn := "amqp://" + r.User + ":" + r.Password + "@" + r.Host + ":" + r.Port + r.Vhost 44 | conn, err := amqp.Dial(dsn) 45 | failOnError(err, "Failed to connect to RabbitMQ") 46 | 47 | r.Channel, err = conn.Channel() 48 | failOnError(err, "Failed to open a channel") 49 | 50 | return r.Channel 51 | } 52 | 53 | func (r *RabbitMQ) Consume(messageChannel chan amqp.Delivery) { 54 | 55 | q, err := r.Channel.QueueDeclare( 56 | r.ConsumerQueueName, // name 57 | true, // durable 58 | false, // delete when usused 59 | false, // exclusive 60 | false, // no-wait 61 | r.Args, // arguments 62 | ) 63 | failOnError(err, "failed to declare a queue") 64 | 65 | incomingMessage, err := r.Channel.Consume( 66 | q.Name, // queue 67 | r.ConsumerName, // consumer 68 | r.AutoAck, // auto-ack 69 | false, // exclusive 70 | false, // no-local 71 | false, // no-wait 72 | nil, // args 73 | ) 74 | failOnError(err, "Failed to register a consumer") 75 | 76 | go func() { 77 | for message := range incomingMessage { 78 | log.Println("Incoming new message") 79 | messageChannel <- message 80 | } 81 | log.Println("RabbitMQ channel closed") 82 | close(messageChannel) 83 | }() 84 | } 85 | 86 | func (r *RabbitMQ) Notify(message string, contentType string, exchange string, routingKey string) error { 87 | 88 | err := r.Channel.Publish( 89 | exchange, // exchange 90 | routingKey, // routing key 91 | false, // mandatory 92 | false, // immediate 93 | amqp.Publishing{ 94 | ContentType: contentType, 95 | Body: []byte(message), 96 | }) 97 | 98 | if err != nil { 99 | return err 100 | } 101 | 102 | return nil 103 | } 104 | 105 | func failOnError(err error, msg string) { 106 | if err != nil { 107 | log.Fatalf("%s: %s", msg, err) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /framework/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "encoding/json" 4 | 5 | func IsJson(s string) error { 6 | var js struct{} 7 | 8 | if err := json.Unmarshal([]byte(s), &js); err != nil { 9 | return err 10 | } 11 | 12 | return nil 13 | } -------------------------------------------------------------------------------- /framework/utils/utils_test.go: -------------------------------------------------------------------------------- 1 | package utils_test 2 | 3 | import ( 4 | "encoder/framework/utils" 5 | "github.com/stretchr/testify/require" 6 | "testing" 7 | ) 8 | 9 | func TestIsJson(t *testing.T) { 10 | json := `{ 11 | "id": "525b5fd9-700d-4feb-89c0-415a1e6e148c", 12 | "file_path": "convite.mp4", 13 | "status": "pending" 14 | }` 15 | 16 | err := utils.IsJson(json) 17 | require.Nil(t, err) 18 | 19 | json = `wes` 20 | err = utils.IsJson(json) 21 | require.Error(t, err) 22 | } 23 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module encoder 2 | 3 | go 1.13 4 | 5 | require ( 6 | cloud.google.com/go/storage v1.9.0 7 | github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 8 | github.com/jinzhu/gorm v1.9.12 9 | github.com/joho/godotenv v1.3.0 10 | github.com/lib/pq v1.1.1 11 | github.com/satori/go.uuid v1.2.0 12 | github.com/streadway/amqp v1.0.0 13 | github.com/stretchr/testify v1.6.1 14 | ) 15 | -------------------------------------------------------------------------------- /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.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 7 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 8 | cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= 9 | cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= 10 | cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= 11 | cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= 12 | cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= 13 | cloud.google.com/go v0.57.0 h1:EpMNVUorLiZIELdMZbCYX/ByTFCdoYopYAGxaGVz9ms= 14 | cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= 15 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 16 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= 17 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= 18 | cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= 19 | cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= 20 | cloud.google.com/go/bigquery v1.8.0 h1:PQcPefKFdaIzjQFbiyOgAqyx8q5djaE7x9Sqe712DPA= 21 | cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= 22 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 23 | cloud.google.com/go/datastore v1.1.0 h1:/May9ojXjRkPBNVrq+oWLqmWCkr4OU5uRY29bu0mRyQ= 24 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= 25 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 26 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= 27 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= 28 | cloud.google.com/go/pubsub v1.3.1 h1:ukjixP1wl0LpnZ6LWtZJ0mX5tBmjp1f8Sqer8Z2OMUU= 29 | cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= 30 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 31 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= 32 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= 33 | cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= 34 | cloud.google.com/go/storage v1.9.0 h1:oXnZyBjHB6hC8TnSle0AWW6pGJ29EuSo5ww+SFmdNBg= 35 | cloud.google.com/go/storage v1.9.0/go.mod h1:m+/etGaqZbylxaNT876QGXqEHp4PR2Rq5GMqICWb9bU= 36 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 37 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 38 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 39 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 40 | github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 h1:4daAzAu0S6Vi7/lbWECcX0j45yZReDZ56BQsrVBOEEY= 41 | github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= 42 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 43 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 44 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 45 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 46 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 47 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 48 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 49 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 50 | github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM= 51 | github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= 52 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 53 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 54 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 55 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 56 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= 57 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= 58 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 59 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 60 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 61 | github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= 62 | github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= 63 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= 64 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= 65 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 66 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 67 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 68 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= 69 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 70 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 71 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 72 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 73 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 74 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 75 | github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 76 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 77 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 78 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 79 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 80 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 81 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 82 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 83 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 84 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 85 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 86 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 87 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 88 | github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= 89 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 90 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 91 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 92 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 93 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 94 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 95 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 96 | github.com/google/go-cmp v0.4.1 h1:/exdXoGamhu5ONeUJH0deniYLWYvQwW66yvlfiiKTu0= 97 | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 98 | github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= 99 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 100 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 101 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 102 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 103 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 104 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 105 | github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 106 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 107 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 108 | github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= 109 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 110 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 111 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 112 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 113 | github.com/jinzhu/gorm v1.9.12 h1:Drgk1clyWT9t9ERbzHza6Mj/8FY/CqMyVzOiHviMo6Q= 114 | github.com/jinzhu/gorm v1.9.12/go.mod h1:vhTjlKSJUTWNtcbQtrMBFCxy7eXTzeCAzfL5fBZT/Qs= 115 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 116 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 117 | github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M= 118 | github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 119 | github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= 120 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= 121 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 122 | github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= 123 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 124 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 125 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 126 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 127 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 128 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 129 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 130 | github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4= 131 | github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 132 | github.com/mattn/go-sqlite3 v2.0.1+incompatible h1:xQ15muvnzGBHpIpdrNi1DA5x0+TcBZzsIDwmw9uTHzw= 133 | github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 134 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 135 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 136 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 137 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 138 | github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= 139 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 140 | github.com/streadway/amqp v1.0.0 h1:kuuDrUJFZL1QYL9hUNuCxNObNzB0bV/ZG5jV3RWAQgo= 141 | github.com/streadway/amqp v1.0.0/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= 142 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 143 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 144 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 145 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 146 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 147 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 148 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 149 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 150 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 151 | go.opencensus.io v0.22.3 h1:8sGtKOrtQqkN1bp2AtX+misvLIlOmsEsNd+9NIcPEm8= 152 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 153 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 154 | golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 155 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 156 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 157 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 158 | golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd h1:GGJVjV8waZKRHrgwvtH66z9ZGVurTD1MT0n1Bb+q4aM= 159 | golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 160 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 161 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 162 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 163 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 164 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 165 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 166 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 167 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 168 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= 169 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= 170 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 171 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 172 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 173 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 174 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 175 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 176 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 177 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 178 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 179 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 180 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 181 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k= 182 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 183 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 184 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 185 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 186 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 187 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 188 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 189 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 190 | golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= 191 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 192 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 193 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 194 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 195 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 196 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 197 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 198 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 199 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 200 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 201 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 202 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 203 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 204 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 205 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 206 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 207 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 208 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 209 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 210 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 211 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 212 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 213 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2 h1:eDrdRpKgkcCqKZQwyZRyeFZgfqt37SL7Kv3tok06cKE= 214 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 215 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 216 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 217 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 218 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 219 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= 220 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 221 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 222 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 223 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 224 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 225 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 226 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 227 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o= 228 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 229 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 230 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 231 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 232 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 233 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 234 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 235 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 236 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 237 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 238 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 239 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 240 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 241 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 242 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 243 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 244 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 245 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 246 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 247 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 248 | golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 249 | golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 250 | golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 251 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 252 | golang.org/x/sys v0.0.0-20200523222454-059865788121 h1:rITEj+UZHYC927n8GT97eC3zrpzXdb/voyeOuVKS46o= 253 | golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 254 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 255 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 256 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 257 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 258 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 259 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 260 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 261 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 262 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 263 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 264 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 265 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 266 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 267 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 268 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 269 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 270 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 271 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 272 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 273 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 274 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 275 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 276 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 277 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 278 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 279 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 280 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 281 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 282 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 283 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 284 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 285 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 286 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 287 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 288 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 289 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 290 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 291 | golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 292 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 293 | golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 294 | golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= 295 | golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 296 | golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 297 | golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 298 | golang.org/x/tools v0.0.0-20200601175630-2caf76543d99 h1:deddXmhOJb/bvD/4M/j2AUMrhHeh6GkqykJSCWyTNVk= 299 | golang.org/x/tools v0.0.0-20200601175630-2caf76543d99/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 300 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 301 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 302 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 303 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 304 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 305 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 306 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 307 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 308 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 309 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 310 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 311 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 312 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 313 | google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 314 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 315 | google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 316 | google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 317 | google.golang.org/api v0.26.0 h1:VJZ8h6E8ip82FRpQl848c5vAadxlTXrUh8RzQzSRm08= 318 | google.golang.org/api v0.26.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 319 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 320 | google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= 321 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 322 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 323 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 324 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 325 | google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= 326 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 327 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 328 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 329 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 330 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 331 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 332 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 333 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 334 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 335 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 336 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 337 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 338 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 339 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 340 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 341 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= 342 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 343 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 344 | google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 345 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 346 | google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 347 | google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 348 | google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 349 | google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 350 | google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= 351 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 352 | google.golang.org/genproto v0.0.0-20200603110839-e855014d5736 h1:+IE3xTD+6Eb7QWG5JFp+dQr/XjKpjmrNkh4pdjTdHEs= 353 | google.golang.org/genproto v0.0.0-20200603110839-e855014d5736/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= 354 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 355 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 356 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 357 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 358 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 359 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 360 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 361 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 362 | google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= 363 | google.golang.org/grpc v1.29.1 h1:EC2SB8S04d2r73uptxphDSUG+kTKVgjRPF+N3xpxRB4= 364 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= 365 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 366 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 367 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 368 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 369 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 370 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 371 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 372 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 373 | google.golang.org/protobuf v1.24.0 h1:UhZDfRO8JRQru4/+LlLE0BRKGF8L+PICnvYZmx/fEGA= 374 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 375 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 376 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 377 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 378 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 379 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 380 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 381 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 382 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 383 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 384 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 385 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 386 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 387 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 388 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 389 | honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8= 390 | honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 391 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 392 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= 393 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 394 | --------------------------------------------------------------------------------