diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..7ef229d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +.git +.gitkeep +.gitignore +.idea +.vscode +third_party/* +*.pb.go +temp +*.md +env.list +Dockerfile +protoc_options_generate.txt +*.env diff --git a/.gitignore b/.gitignore index 960c9b9..6b82657 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ .vscode third_party/* *.pb.go +*.env diff --git a/Dockerfile.app b/Dockerfile.app new file mode 100644 index 0000000..0348a03 --- /dev/null +++ b/Dockerfile.app @@ -0,0 +1,13 @@ +FROM golang:1.19.3-alpine as builder +WORKDIR /app/src +COPY go.mod go.sum . +RUN go mod download && go mod verify +COPY . . +RUN CGO_ENABLED=0 GOOS=linux go build -o /app/bin/certificate-server /app/src/cmd/rpc +### +FROM surnet/alpine-wkhtmltopdf:3.16.2-0.12.6-small +WORKDIR /app/bin +COPY --from=builder /app/bin/certificate-server /app/bin/certificate-server +RUN mkdir -p /storage +ENV STORAGE=local +ENTRYPOINT ["./certificate-server"] diff --git a/Dockerfile.proto b/Dockerfile.proto new file mode 100644 index 0000000..ba35067 --- /dev/null +++ b/Dockerfile.proto @@ -0,0 +1,23 @@ +FROM golang:1.19.3 +WORKDIR /app +RUN apt-get update; \ +apt-get install -y wget unzip +RUN wget -P /tmp https://github.com/protocolbuffers/protobuf/releases/download/v21.9/protoc-21.9-linux-x86_64.zip +RUN unzip /tmp/protoc-21.9-linux-x86_64.zip -d ./third_party +# +RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28 +RUN go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2 +# +RUN mkdir -p ./protobuf/transport/certificate /result +COPY ./api/proto/* ./api/proto/ +RUN third_party/bin/protoc \ +-Iapi/proto \ +-Ithird_party/include \ +--proto_path=api/proto \ +--go_out=protobuf/transport/certificate \ +--go_opt=paths=source_relative \ +--go-grpc_out=protobuf/transport/certificate \ +--go-grpc_opt=paths=source_relative \ +api/proto/*.proto +# +ENTRYPOINT cp /app/protobuf/transport/certificate/* /result/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..ff3dc0f --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +# Certificates service +=== + +### Запуск сервиса в Docker: +#### Создание образа и генерация **protobuf** файлов из **".proto"**: +`docker image build -f Dockerfile.proto -t cert-proto .` + +#### Сохранение в локальном проекте **protobuf** файлов из Docker контейнера **cert-proto**: +`docker container run --rm -v $PWD/protobuf/transport/certificate:/result cert-proto` // Linux +`docker container run --rm -v %CD%/protobuf/transport/certificate:/result cert-proto` // Windows + +#### Создание образа для запуска проекта: +`docker image build -f Dockerfile.app -t cert-app .` + +#### Запуск проекта с сохранением в Local Storage: +`docker container run --rm --env-file env.list -p 1234:1234 -it -v certificate-storage:/storage cert-app` +В файле **env.list** задаются переменные откружения необходимые для запуска сервиса. +Тип Storage задается перемнной **STORAGE=local**, переменную **STORAGE** можно опустить, значение **local** будет присвоено по умолчанию. +Монтируется Docker VOLUME: **certificate-storage** на локальной машине/сервере, для сохранения шаблонов и сертификатов. + +#### Запуск проекта с сохранением в AWS S3 : +`docker container run --rm --env-file env.list --env-file s3.env -p 1234:1234 -it cert-app` +В файле **env.list** обязательно задается переменная окружения **STORAGE=s3**. +В файле **s3.env** обязательно задаються переменные окружения: +``` +S3_ENDPOINT=... // s3.amazonaws.com - если планируем использовать облачное хранилище от Amazon. +S3_BUCKET_NAME=... // Имя существующего Bucket, на который у вас есть права записи. +ACCESS_KEY_ID=... +SECRET_ACCESS_KEY=... +``` +### Требования к шаблонам: +В шаблоне могут содержаться следующие теги замены: +``` +{{.CourseName}} +{{.CourseType}} +{{.CourseHours}} +{{.CourseDate}} +{{.CourseMentors}} +{{.StudentFirstname}} +{{.StudentLastname}} +{{.QrCodeLink}} +``` +Шаблоны с любыми другими тегами замены будут отклонены валидатором. +Вместо тега замены `{{.QrCodeLink}}` будет вставлен **QR** код в формате **PNG**: ссылка на сертификат. + +### Пример простого HTML шаблона: +``` +
{{.CourseName}}
+{{.CourseType}}
+{{.CourseHours}}
+{{.CourseDate}}
+{{.CourseMentors}}
+{{.StudentFirstname}}
+{{.StudentLastname}}
+{{.CourseName}}
{{.CourseType}}
{{.CourseHours}}
{{.CourseDate}}
{{.CourseMentors}}
{{.StudentFirstname}}
{{.StudentLastname}}
`), failTemplate: []byte(`{{.CourseName_Fail}}
`), - expected: []byte(`Golang
Theory
35
25.01.2023
Pavel Gordiyanov, Mikita Viarbovikau, Sergey Shtripling
Ivan
Ivanov
`), + expectedId: "612364afe471b3b1cc80083183fd381d", } func TestMain(m *testing.M) { @@ -52,7 +54,32 @@ func TestGenerateCertificate(t *testing.T) { t.Error(err) } - if !reflect.DeepEqual(gotCertif, testData.expected) { + if !reflect.DeepEqual(gotCertif, testData.expectedCert) { t.Errorf("%q and %q should be equal", "gotSertif", "expectedCert") } } + +func TestGenerateID(t *testing.T) { + generator := testData.certGenerator + + actualId := generator.GenerateID() + if actualId != testData.expectedId { + t.Errorf("expected:%q, actual:%q", testData.expectedId, actualId) + } +} + +func TestCheckTemplateHTML_fail(t *testing.T) { + generator := testData.certGenerator + err := generator.CheckTemplateHTML(testData.failTemplate) + if err == nil { + t.Error("err must not be nil") + } +} + +func TestCheckTemplateHTML(t *testing.T) { + generator := testData.certGenerator + err := generator.CheckTemplateHTML(testData.goodTemplate) + if err != nil { + t.Error(err) + } +} diff --git a/app/idgenerator/README.md b/app/idgenerator/README.md deleted file mode 100644 index 305a7bc..0000000 --- a/app/idgenerator/README.md +++ /dev/null @@ -1,2 +0,0 @@ -Пакет, отвечающий за генерацию имени файла сертификата -=== \ No newline at end of file diff --git a/app/server/README.md b/app/server/README.md deleted file mode 100644 index 8f639e1..0000000 --- a/app/server/README.md +++ /dev/null @@ -1,2 +0,0 @@ -Реализация функций gRPC сервера -=== \ No newline at end of file diff --git a/app/server/server.go b/app/server/server.go new file mode 100644 index 0000000..5f01fbc --- /dev/null +++ b/app/server/server.go @@ -0,0 +1,228 @@ +package server + +import ( + "context" + "fmt" + "os" + "strings" + + valid "github.com/go-ozzo/ozzo-validation/v4" + validIs "github.com/go-ozzo/ozzo-validation/v4/is" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "gus_certificates/app/certgenerator" + certSPb "gus_certificates/protobuf/transport/certificate" + "gus_certificates/utils/pdfgenerator" + "gus_certificates/utils/qrgenerator" + "gus_certificates/utils/storage" +) + +var fileNameRule = []valid.Rule{ + valid.Required, + valid.RuneLength(5, 255), + validIs.ASCII, +} + +const ( + envStorageType = "STORAGE" // ["local", "s3"] Хранение файлов в локальном хранилище или на s3. + + certificateFileExtension = ".pdf" +) + +type certificateServer struct { + certSPb.UnimplementedCertificateServer + + // Генерация сертификата в HTML, генерация ID сертификата. + certGen *certgenerator.CertGenerator + + // Конвертация сертификата в формат PDF. + pdfGen pdfgenerator.PdfGenerator + + // Работа с файловым хранилищем. + strg storage.Storage + + // Генерация QR кодов. + qrGen *qrgenerator.QrGenerator +} + +func NewCertificateServer() (*certificateServer, error) { + pdfGen, err := pdfgenerator.New() + if err != nil { + return nil, err + } + + strg, err := newStorage() + if err != nil { + return nil, err + } + + server := &certificateServer{} + server.certGen = &certgenerator.CertGenerator{} + server.pdfGen = pdfGen + server.strg = strg + server.qrGen = &qrgenerator.QrGenerator{} + + return server, nil +} + +func newStorage() (storage.Storage, error) { + storageType := strings.ToLower(os.Getenv(envStorageType)) + + if storageType == "local" { + strg, err := storage.NewLocal() + if err != nil { + return nil, err + } + return strg, nil + } + + if storageType == "s3" { + strg, err := storage.NewS3() + if err != nil { + return nil, err + } + return strg, nil + } + return nil, fmt.Errorf("environment variable:%s=%q, only [local, s3] values are allowed", envStorageType, storageType) +} + +func (c *certificateServer) fillData(course *certSPb.CourseMessage, student *certSPb.StudentMessage) { + c.certGen.SetCourseName(course.GetCourseName()) + c.certGen.SetCourseType(course.GetCourseType()) + c.certGen.SetCourseHours(course.GetHours()) + c.certGen.SetCourseDate(course.GetDate()) + c.certGen.SetCourseMentors(course.GetMentors()) + c.certGen.SetStudentFirstname(student.GetFirstname()) + c.certGen.SetStudentLastname(student.GetLastname()) +} + +func (c *certificateServer) IssueCertificate(ctx context.Context, req *certSPb.IssueCertificateReq) (*certSPb.IssueCertificateResp, error) { + // Валидация имени шаблона. + templateName := req.GetTemplateName() + err := valid.Validate(templateName, fileNameRule...) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "%s: %q %v", "IssueCertificate", templateName, err) + } + + // Заполнение и валидация данных о курсе и студенте. + c.fillData(req.GetCourse(), req.GetStudent()) + err = c.certGen.ValidateData() + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "%s: %v", "IssueCertificate", err) + } + + // Генерация ID сертификата. + certificateId := c.certGen.GenerateID() + + // Генерация QR-Code на линк сертификата. + qrCodeLinkPNG, err := c.qrGen.GenerateQrPNG(certificateId) // Пока не реализовано получение линка передается имя сертификата + if err != nil { + return nil, status.Errorf(codes.FailedPrecondition, "%s: %v", "IssueCertificate", err) + } + c.certGen.SetQrCodeLink(qrCodeLinkPNG) + + // Получение шабона из хранилища. + template, err := c.strg.GetTemplate(templateName) + if err != nil { + return nil, status.Errorf(codes.FailedPrecondition, "%s: %q %v", "IssueCertificate", templateName, err) + } + + // Генерация сертификата в формате HTML. + certificate, err := c.certGen.GenerateCertHTML(template) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "%s: %v", "IssueCertificate", err) + } + + // Конвертация сертификата в формат PDF. + certificatePDF, err := c.pdfGen.RenderHtmlToPdf(certificate) + if err != nil { + return nil, status.Errorf(codes.FailedPrecondition, "%s: %v", "IssueCertificate", err) + } + + // Сохранение сертификата в хранилище. + err = c.strg.SaveCertificate(certificateId+certificateFileExtension, certificatePDF) + if err != nil { + return nil, status.Errorf(codes.FailedPrecondition, "%s: %v", "IssueCertificate", err) + } + + resp := &certSPb.IssueCertificateResp{Id: certificateId} + return resp, nil +} + +func (c *certificateServer) GetCertificateFileByID(ctx context.Context, req *certSPb.GetCertificateFileByIDReq) (*certSPb.GetCertificateFileByIDResp, error) { + // Валидация имени сертификата. + certificateId := req.GetId() + err := valid.Validate(certificateId, fileNameRule...) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "%s: %q %v", "GetCertificateFileByID", certificateId, err) + } + + // Получение сертификата из хранилища. + certificate, err := c.strg.GetCertificate(certificateId + certificateFileExtension) + if err != nil { + return nil, status.Errorf(codes.FailedPrecondition, "%s: %q %v", "GetCertificateFileByID", certificateId, err) + } + + resp := &certSPb.GetCertificateFileByIDResp{Certificate: certificate} + return resp, nil +} + +func (c *certificateServer) GetCertificateLinkByID(ctx context.Context, req *certSPb.GetCertificateLinkByIDReq) (*certSPb.GetCertificateLinkByIDResp, error) { + // Валидация имени сертификата. + certificateId := req.GetId() + err := valid.Validate(certificateId, fileNameRule...) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "%s: %q %v", "GetCertificateLinkByID", certificateId, err) + } + + // Получение линка на сертификат. + certificateLink := certificateId // Пока не реализовано получение линка передается имя сертификата + + resp := &certSPb.GetCertificateLinkByIDResp{Link: certificateLink} + // resp := &certSPb.GetCertificateLinkByIDResp{Link: certificateFullPath} + return resp, nil +} + +func (c *certificateServer) AddTemplate(ctx context.Context, req *certSPb.AddTemplateReq) (*certSPb.AddTemplateResp, error) { + // Валидация имени шаблона. + templateName := req.GetTemplateName() + err := valid.Validate(templateName, fileNameRule...) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "%s: %q %v", "AddTemplate", templateName, err) + } + + // Проверка корректности файла шаблона. + templateByte := req.GetTemplate() + err = c.certGen.CheckTemplateHTML(templateByte) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "%s: %q %v", "AddTemplate", templateName, err) + } + + // Сохранение шаблона в хранилище. + err = c.strg.SaveTemplate(templateName, templateByte) + if err != nil { + return nil, status.Errorf(codes.FailedPrecondition, "%s: %q %v", "AddTemplate", templateName, err) + } + + resp := &certSPb.AddTemplateResp{Status: &certSPb.Status{Code: int32(codes.OK)}} + return resp, nil +} + +func (c *certificateServer) DeleteTemplate(ctx context.Context, req *certSPb.DeleteTemplateReq) (*certSPb.DeleteTemplateResp, error) { + // Валидация имени шаблона. + templateName := req.GetTemplateName() + err := valid.Validate(templateName, fileNameRule...) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "%s: %q %v", "DelTemplate", templateName, err) + } + + // Удаление шаблона из хранилища. + err = c.strg.DeleteTemplate(templateName) + if err != nil { + return nil, status.Errorf(codes.FailedPrecondition, "%s: %q %v", "DelTemplate", templateName, err) + } + + resp := &certSPb.DeleteTemplateResp{Status: &certSPb.Status{Code: int32(codes.OK)}} + return resp, nil +} diff --git a/app/transport/README.md b/app/transport/README.md deleted file mode 100644 index 2cd50bd..0000000 --- a/app/transport/README.md +++ /dev/null @@ -1,2 +0,0 @@ -Сгенерированные файлы grpc -=== \ No newline at end of file diff --git a/cmd/rpc/main.go b/cmd/rpc/main.go new file mode 100644 index 0000000..2913d6d --- /dev/null +++ b/cmd/rpc/main.go @@ -0,0 +1,46 @@ +package main + +import ( + "fmt" + "log" + "net" + "os" + + "google.golang.org/grpc" + + "gus_certificates/app/server" + certSPb "gus_certificates/protobuf/transport/certificate" +) + +func main() { + host := os.Getenv("CERTIFICATES_HOST") + if host == "" { + log.Fatal(fmt.Errorf("environment variable %q not set", "CERTIFICATES_HOST")) + } + + port := os.Getenv("CERTIFICATES_PORT") + if port == "" { + log.Fatal(fmt.Errorf("environment variable %q not set", "CERTIFICATES_PORT")) + } + + err := runRpcServer(net.JoinHostPort(host, port)) + if err != nil { + log.Fatal(err) + } +} + +func runRpcServer(addr string) error { + listener, err := net.Listen("tcp", addr) + if err != nil { + return err + } + + grpcServer := grpc.NewServer() + server, err := server.NewCertificateServer() + if err != nil { + return err + } + + certSPb.RegisterCertificateServer(grpcServer, server) + return grpcServer.Serve(listener) +} diff --git a/env.list b/env.list new file mode 100644 index 0000000..763cdec --- /dev/null +++ b/env.list @@ -0,0 +1,6 @@ +# local or s3 +STORAGE=local +TEMPLATES_DIR=/storage/templates +CERTIFICATES_DIR=/storage/certificates +CERTIFICATES_HOST=0.0.0.0 +CERTIFICATES_PORT=1234 diff --git a/go.mod b/go.mod index 3c75d33..91cba28 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,37 @@ module gus_certificates go 1.19 -require github.com/SebastiaanKlippert/go-wkhtmltopdf v1.7.2 +require ( + github.com/SebastiaanKlippert/go-wkhtmltopdf v1.7.2 + github.com/go-ozzo/ozzo-validation/v4 v4.3.0 + github.com/minio/minio-go/v7 v7.0.44 + google.golang.org/grpc v1.50.1 + google.golang.org/protobuf v1.28.1 +) + +require ( + github.com/dustin/go-humanize v1.0.0 // indirect + github.com/google/go-cmp v0.5.8 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.15.9 // indirect + github.com/klauspost/cpuid/v2 v2.1.0 // indirect + github.com/minio/md5-simd v1.1.2 // indirect + github.com/minio/sha256-simd v1.0.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/rs/xid v1.4.0 // indirect + github.com/sirupsen/logrus v1.9.0 // indirect + golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect + gopkg.in/ini.v1 v1.66.6 // indirect +) + +require ( + github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e + golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect + golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect + golang.org/x/text v0.3.7 // indirect + google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect +) diff --git a/go.sum b/go.sum index 7189542..0ac71a0 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,134 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/SebastiaanKlippert/go-wkhtmltopdf v1.7.2 h1:LORAatv6KuKheYq8HXehiwx3f/VGuzJBNSydUDQ98EM= github.com/SebastiaanKlippert/go-wkhtmltopdf v1.7.2/go.mod h1:TY8r0gmwEL1c5Lbd66NgQCkL4ZjGDJCMVqvbbFvUx20= +github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 h1:zV3ejI06GQ59hwDQAvmK1qxOQGB3WuVTRoY0okPTAv0= +github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es= +github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY= +github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= +github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.1.0 h1:eyi1Ad2aNJMW95zcSbmGg7Cg6cq3ADwLpMAP96d8rF0= +github.com/klauspost/cpuid/v2 v2.1.0/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= +github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.0.44 h1:9zUJ7iU7ax2P1jOvTp6nVrgzlZq3AZlFm0XfRFDKstM= +github.com/minio/minio-go/v7 v7.0.44/go.mod h1:nCrRzjoSUQh8hgKKtu3Y708OLvRLtuASMg2/nvmbarw= +github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= +github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY= +github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.50.1 h1:DS/BukOZWp8s6p4Dt/tOaJaTQyPyOoCcrjroHuCeLzY= +google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ini.v1 v1.66.6 h1:LATuAqN/shcYAOkv3wl2L4rkaKqkcgTBQjOyYDvcPKI= +gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/utils/storage/local/.gitkeep b/protobuf/transport/certificate/.gitkeep similarity index 100% rename from utils/storage/local/.gitkeep rename to protobuf/transport/certificate/.gitkeep diff --git a/protoc_options_generate.txt b/protoc_options_generate.txt new file mode 100644 index 0000000..2fb772d --- /dev/null +++ b/protoc_options_generate.txt @@ -0,0 +1,8 @@ +-Iapi/proto +-Ithird_party/include +--proto_path=api/proto +--go_out=protobuf/transport/certificate +--go_opt=paths=source_relative +--go-grpc_out=protobuf/transport/certificate +--go-grpc_opt=paths=source_relative +api/proto/*.proto diff --git a/utils/pdfgenerator/htmltopdf/htmltopdf.go b/utils/pdfgenerator/htmltopdf/htmltopdf.go index e8d73b3..78a8b50 100644 --- a/utils/pdfgenerator/htmltopdf/htmltopdf.go +++ b/utils/pdfgenerator/htmltopdf/htmltopdf.go @@ -27,7 +27,9 @@ func (h *htmltopdf) RenderHtmlToPdf(htmlBytes []byte) ([]byte, error) { h.pdfgenerator.Buffer().Reset() bytesReader := bytes.NewReader(htmlBytes) - h.pdfgenerator.AddPage(wkhtmltopdf.NewPageReader(bytesReader)) + pageReader := wkhtmltopdf.NewPageReader(bytesReader) + pageReader.Encoding.Set("utf-8") + h.pdfgenerator.AddPage(pageReader) err := h.pdfgenerator.Create() if err != nil { return nil, err diff --git a/utils/qrgenerator/qrgenerator.go b/utils/qrgenerator/qrgenerator.go new file mode 100644 index 0000000..b74324b --- /dev/null +++ b/utils/qrgenerator/qrgenerator.go @@ -0,0 +1,16 @@ +package qrgenerator + +import "github.com/skip2/go-qrcode" + +const sizeImage = 128 + +type QrGenerator struct { +} + +func (q *QrGenerator) GenerateQrPNG(sourceString string) ([]byte, error) { + data, err := qrcode.Encode(sourceString, qrcode.Medium, sizeImage) + if err != nil { + return nil, err + } + return data, nil +} diff --git a/utils/qrgenerator/qrgenerator_test.go b/utils/qrgenerator/qrgenerator_test.go new file mode 100644 index 0000000..31ecbb6 --- /dev/null +++ b/utils/qrgenerator/qrgenerator_test.go @@ -0,0 +1,18 @@ +package qrgenerator + +import "testing" + +func TestGenerateQrPNG(t *testing.T) { + qrGen := QrGenerator{} + + testStringLink := "https://example.com/certificate1234.pdf" + + data, err := qrGen.GenerateQrPNG(testStringLink) + if err != nil { + t.Error(err) + } + + if len(data) == 0 { + t.Error("len(data): must be greater than 0") + } +} diff --git a/utils/storage/local/local.go b/utils/storage/local/local.go index 237a74d..2007c50 100644 --- a/utils/storage/local/local.go +++ b/utils/storage/local/local.go @@ -1,9 +1,7 @@ package local import ( - "errors" "fmt" - "io/fs" "os" "path" ) @@ -13,9 +11,16 @@ const ( envCertificatesDir = "CERTIFICATES_DIR" ) +// Оптимизация скорости. Структура для сохранения в памяти последнего запрошенного шаблона, возращает при повторных запросах. +type lastRequestTemplate struct { + name string + data []byte +} + type localStorage struct { templatesDir string certificatesDir string + lastTemplate lastRequestTemplate } func New() (*localStorage, error) { @@ -40,14 +45,28 @@ func New() (*localStorage, error) { ls := &localStorage{} ls.templatesDir = templatesDir ls.certificatesDir = certificatesDir + ls.lastTemplate = lastRequestTemplate{} return ls, nil } func (l *localStorage) GetTemplate(fileName string) ([]byte, error) { + // Возвращаем из памяти если уже запрашивали. + if l.lastTemplate.name == fileName { + return l.lastTemplate.data, nil + } + fullPath := path.Join(l.templatesDir, fileName) - return getFile(fullPath) + data, err := getFile(fullPath) + if err != nil { + return nil, err + } + + // Обновляем в памяти последний запрошенный. + l.lastTemplate.name = fileName + l.lastTemplate.data = data + return data, nil } func (l *localStorage) GetCertificate(fileName string) ([]byte, error) { @@ -55,19 +74,19 @@ func (l *localStorage) GetCertificate(fileName string) ([]byte, error) { return getFile(fullPath) } -func (l *localStorage) GetCertificatePath(fileName string) (string, error) { - fullPath := path.Join(l.certificatesDir, fileName) - - if _, err := os.Stat(fullPath); errors.Is(err, fs.ErrNotExist) { - return "", err - } - return fullPath, nil -} - func (l *localStorage) SaveTemplate(fileName string, data []byte) error { fullPath := path.Join(l.templatesDir, fileName) - return saveFile(fullPath, data) + err := saveFile(fullPath, data) + if err != nil { + return err + } + + // Обновляем в памяти последний сохраненный при обновлении. + if l.lastTemplate.name == fileName { + l.lastTemplate.data = data + } + return nil } func (l *localStorage) SaveCertificate(fileName string, data []byte) error { @@ -76,6 +95,12 @@ func (l *localStorage) SaveCertificate(fileName string, data []byte) error { } func (l *localStorage) DeleteTemplate(fileName string) error { + // Очищаем в памяти последний сохраненный при его удалении. + if l.lastTemplate.name == fileName { + l.lastTemplate.name = "" + l.lastTemplate.data = nil + } + fullPath := path.Join(l.templatesDir, fileName) return deleteFile(fullPath) diff --git a/utils/storage/local/local_test.go b/utils/storage/local/local_test.go index e78b9c9..ab02f19 100644 --- a/utils/storage/local/local_test.go +++ b/utils/storage/local/local_test.go @@ -2,7 +2,6 @@ package local import ( "os" - "path" "reflect" "testing" ) @@ -139,32 +138,3 @@ func TestCertificatesOperations(t *testing.T) { t.Error(err) } } - -func TestGetCertificatePath(t *testing.T) { - testData, err := generateTestData(currentWorkDir, currentWorkDir) - if err != nil { - t.Fatal(err) - } - - testFileName := "testDataCertificates.tmp" - testBytes := []byte("Test Certificates Operations") - err = testData.SaveCertificate(testFileName, testBytes) - if err != nil { - t.Fatal(err) - } - - actualPath, err := testData.GetCertificatePath(testFileName) - if err != nil { - t.Error(err) - } - expectedPath := path.Join(currentWorkDir, testFileName) - - if actualPath != expectedPath { - t.Errorf("expect path:%q, actual path:%q", expectedPath, actualPath) - } - - err = testData.DeleteCertificate(testFileName) - if err != nil { - t.Error(err) - } -} diff --git a/utils/storage/s3/.gitkeep b/utils/storage/s3/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/utils/storage/s3/s3.go b/utils/storage/s3/s3.go index 3ed7f97..0a3b814 100644 --- a/utils/storage/s3/s3.go +++ b/utils/storage/s3/s3.go @@ -1 +1,216 @@ package s3 + +import ( + "bytes" + "context" + "fmt" + "io" + "os" + "path" + "path/filepath" + + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" +) + +const ( + envTemplatesDir = "TEMPLATES_DIR" + envCertificatesDir = "CERTIFICATES_DIR" + envS3BucketName = "S3_BUCKET_NAME" + envS3Endpoint = "S3_ENDPOINT" + envAccessKeyID = "ACCESS_KEY_ID" + envSecretAccessKey = "SECRET_ACCESS_KEY" +) + +// Оптимизация скорости. Структура для сохранения в памяти последнего запрошенного шаблона, возращает при повторных запросах. +type lastRequestTemplate struct { + name string + data []byte +} + +type s3Storage struct { + templatesDir string + certificatesDir string + s3BucketName string + s3Client *minio.Client + lastTemplate lastRequestTemplate +} + +func New() (*s3Storage, error) { + templatesDir := filepath.Base(os.Getenv(envTemplatesDir)) + if templatesDir == "" { + return nil, fmt.Errorf("environment variable %q not set", envTemplatesDir) + } + + certificatesDir := filepath.Base(os.Getenv(envCertificatesDir)) + if certificatesDir == "" { + return nil, fmt.Errorf("environment variable %q not set", envCertificatesDir) + } + + s3BucketName := os.Getenv(envS3BucketName) + if s3BucketName == "" { + return nil, fmt.Errorf("environment variable %q not set", envS3BucketName) + } + + s3Client, err := newS3Client(s3BucketName) + if err != nil { + return nil, err + } + + s3s := &s3Storage{} + s3s.templatesDir = templatesDir + s3s.certificatesDir = certificatesDir + s3s.s3BucketName = s3BucketName + s3s.s3Client = s3Client + s3s.lastTemplate = lastRequestTemplate{} + + return s3s, nil +} + +func newS3Client(bucket string) (*minio.Client, error) { + + s3Endpoint := os.Getenv(envS3Endpoint) + if s3Endpoint == "" { + return nil, fmt.Errorf("environment variable %q not set", envS3Endpoint) + } + + accessKeyID := os.Getenv(envAccessKeyID) + if accessKeyID == "" { + return nil, fmt.Errorf("environment variable %q not set", envAccessKeyID) + } + + secretAccessKey := os.Getenv(envSecretAccessKey) + if secretAccessKey == "" { + return nil, fmt.Errorf("environment variable %q not set", envSecretAccessKey) + } + + useSSL := true + options := &minio.Options{Creds: credentials.NewStaticV4(accessKeyID, secretAccessKey, ""), Secure: useSSL} + client, err := minio.New(s3Endpoint, options) + if err != nil { + return nil, err + } + + // Проверяем доступ к Bucket. + ctx := context.Background() + exist, err := client.BucketExists(ctx, bucket) + if err != nil { + return nil, err + } + + if !exist { + return nil, fmt.Errorf("bucket:%q not exist", bucket) + } + + return client, nil +} + +func (s *s3Storage) GetTemplate(fileName string) ([]byte, error) { + // Возвращаем из памяти если уже запрашивали. + if s.lastTemplate.name == fileName { + return s.lastTemplate.data, nil + } + + fullPath := path.Join(s.templatesDir, fileName) + data, err := s.getFile(fullPath) + if err != nil { + return nil, err + } + + // Обновляем в памяти последний запрошенный. + s.lastTemplate.name = fileName + s.lastTemplate.data = data + + return data, nil +} + +func (s *s3Storage) GetCertificate(fileName string) ([]byte, error) { + fullPath := path.Join(s.certificatesDir, fileName) + return s.getFile(fullPath) +} + +func (s *s3Storage) SaveTemplate(fileName string, data []byte) error { + fullPath := path.Join(s.templatesDir, fileName) + err := s.saveFile(fullPath, data) + if err != nil { + return err + } + + // Обновляем в памяти последний сохраненный при обновлении. + if s.lastTemplate.name == fileName { + s.lastTemplate.data = data + } + + return nil +} + +func (s *s3Storage) SaveCertificate(fileName string, data []byte) error { + fullPath := path.Join(s.certificatesDir, fileName) + return s.saveFile(fullPath, data) +} + +func (s *s3Storage) DeleteTemplate(fileName string) error { + // Очищаем в памяти последний сохраненный при его удалении. + if s.lastTemplate.name == fileName { + s.lastTemplate.name = "" + s.lastTemplate.data = nil + } + + fullPath := path.Join(s.templatesDir, fileName) + return s.deleteFile(fullPath) + +} + +func (s *s3Storage) DeleteCertificate(fileName string) error { + fullPath := path.Join(s.certificatesDir, fileName) + return s.deleteFile(fullPath) +} + +func (s *s3Storage) getFile(fullPath string) ([]byte, error) { + ctx := context.Background() + + obj, err := s.s3Client.GetObject(ctx, s.s3BucketName, fullPath, minio.GetObjectOptions{}) + if err != nil { + return nil, err + } + + defer obj.Close() + data, err := io.ReadAll(obj) + if err != nil { + return nil, err + } + + return data, nil +} + +func (s *s3Storage) saveFile(fullPath string, data []byte) error { + ctx := context.Background() + _, err := s.s3Client.PutObject(ctx, s.s3BucketName, fullPath, bytes.NewReader(data), int64(len(data)), minio.PutObjectOptions{}) + if err != nil { + return err + } + return nil +} + +func (s *s3Storage) deleteFile(fullPath string) error { + // Проверяем существует/доступен ли файл. + if err := s.checkFile(fullPath); err != nil { + return err + } + + ctx := context.Background() + err := s.s3Client.RemoveObject(ctx, s.s3BucketName, fullPath, minio.RemoveObjectOptions{}) + if err != nil { + return err + } + return nil +} + +func (s *s3Storage) checkFile(fullPath string) error { + ctx := context.Background() + _, err := s.s3Client.StatObject(ctx, s.s3BucketName, fullPath, minio.StatObjectOptions{}) + if err != nil { + return err + } + return nil +} diff --git a/utils/storage/storage.go b/utils/storage/storage.go index e29efd3..35c9d3a 100644 --- a/utils/storage/storage.go +++ b/utils/storage/storage.go @@ -2,12 +2,12 @@ package storage import ( "gus_certificates/utils/storage/local" + "gus_certificates/utils/storage/s3" ) -type storage interface { +type Storage interface { GetTemplate(string) ([]byte, error) GetCertificate(string) ([]byte, error) - GetCertificatePath(string) (string, error) SaveTemplate(string, []byte) error SaveCertificate(string, []byte) error @@ -16,6 +16,10 @@ type storage interface { DeleteCertificate(string) error } -func NewLocal() (storage, error) { +func NewLocal() (Storage, error) { return local.New() } + +func NewS3() (Storage, error) { + return s3.New() +}