diff --git a/pkg/files/downloader.go b/pkg/files/downloader.go new file mode 100644 index 0000000000000000000000000000000000000000..79acfeed5125a755c6908f77a7b66390239cb534 --- /dev/null +++ b/pkg/files/downloader.go @@ -0,0 +1,32 @@ +package files + +import ( + "errors" + "io" + "net/http" +) + +type Downloader interface { + Download(dst io.Writer, file *File) error +} + +type downloader struct{} + +func NewDownloader() Downloader { + return &downloader{} +} + +func (d *downloader) Download(dst io.Writer, file *File) error { + r, err := http.Get(file.URL) + if err != nil { + return err + } + defer r.Body.Close() + + if r.StatusCode != http.StatusOK { + return errors.New("download request failed: " + r.Status) + } + + _, err = io.Copy(dst, r.Body) + return err +} diff --git a/pkg/files/field.go b/pkg/files/field.go new file mode 100644 index 0000000000000000000000000000000000000000..5ce4dc4ce72506fdd62e184b95487319f95e5122 --- /dev/null +++ b/pkg/files/field.go @@ -0,0 +1,156 @@ +package files + +import ( + "context" + "errors" + "fmt" + "net/url" + "reflect" + + "git.perx.ru/perxis/perxis-go/pkg/items" + "git.perx.ru/perxis/perxis-go/pkg/schema/field" + signer "git.perx.ru/perxis/perxis-go/pkg/urlsigner" + "github.com/mitchellh/mapstructure" +) + +const FileTypeName = "file" + +type FileParameters struct { + t *FileType +} + +func (p FileParameters) Type() field.Type { return p.t } +func (p *FileParameters) Clone(reset bool) field.Parameters { return p } + +type FileType struct { + fs Files + signer signer.URLSigner + fileServerUrl string + uploader Uploader +} + +func NewFileType(fs Files, signer signer.URLSigner, fileServerUrl string) *FileType { + return &FileType{fs: fs, signer: signer, fileServerUrl: fileServerUrl, uploader: NewUploader()} +} + +func (t *FileType) WithUploader(uploader Uploader) *FileType { + t.uploader = uploader + return t +} + +func (t FileType) Name() string { return FileTypeName } + +func (t *FileType) NewParameters() field.Parameters { + return &FileParameters{t} +} + +func (t FileType) Decode(_ context.Context, fld *field.Field, v interface{}) (interface{}, error) { + if v == nil { + return nil, nil + } + var f File + if err := mapstructure.Decode(v, &f); err != nil { + return nil, err + } + return &f, nil +} + +func (t FileType) Encode(ctx context.Context, fld *field.Field, v interface{}) (interface{}, error) { + if v == nil { + return nil, nil + } + + f, ok := v.(*File) + if !ok { + return nil, fmt.Errorf("FileField encode error: incorrect type: \"%s\", expected \"file\"", reflect.ValueOf(v).Kind()) + } + + if f.File != nil { // upload from file system + upload, err := t.fs.Upload(ctx, f) + if err != nil { + return nil, err + } + if err = t.uploader.Upload(f.File, upload); err != nil { + return nil, err + } + f = &upload.File + } + + u := fmt.Sprintf("%s/%s", t.fileServerUrl, f.ID) + resURL, err := url.Parse(u) + if err != nil { + return nil, err + } + if t.signer != nil { + f.URL = t.signer.Sign(resURL).String() + } else { + f.URL = resURL.String() + } + res := make(map[string]interface{}) + if err := mapstructure.Decode(f, &res); err != nil { + return nil, err + } + + return res, nil + +} + +// PreSave - функция Р±СѓРґРµ вызвана перед сохранением поля РІ Storage. Реализует интерфейс `perxis.PreSaver` +// Выполняет проверку поля является ли файл только что загруженным Рё переносит его РїСЂРё необходимости для +// постоянного хранения +func (t FileType) PreSave(ctx context.Context, fld *field.Field, v interface{}, itemCtx *items.Context) (interface{}, bool, error) { + if v == nil { + return nil, false, nil + } + f := v.(*File) + if f.ID == "" { + return nil, false, errors.New("FileType: file id required") + } + if !f.Temporary() { + return f, false, nil + } + + f, err := t.fs.MoveUpload(ctx, &MultipartUpload{File: *f}) + if err != nil { + return nil, false, err + } + return f, true, nil +} + +// Field - создает РЅРѕРІРѕРµ поле Field типа FileType +// FileType должен быть предварительно создан `NewFileType` Рё зарегистрирован `field.Register` +func Field(o ...interface{}) *field.Field { + t, ok := field.GetType(FileTypeName) + if !ok { + panic("field file type not registered") + } + return field.NewField(t.NewParameters(), o...) +} + +func (t *FileType) IsEmpty(v interface{}) bool { + if v == nil { + return true + } + + f, ok := v.(*File) + + return !ok || f.ID == "" +} + +func (p FileParameters) GetField(path string) (fld *field.Field) { + switch path { + case "id", "name", "mimeType", "url", "key": + return field.String() + case "size": + return field.Number(field.NumberFormatInt) + default: + return nil + } +} + +func init() { + // РџРѕ умолчанию без FS + // Если РЅСѓР¶РЅС‹ подписанные URL, Рё загрузка РЅР° FS, РЅСѓР¶РЅРѕ зарегистрировать корректный типа + // РЎРј. cmd/content/command/server.go:195 + field.Register(NewFileType(nil, nil, "")) +} diff --git a/pkg/files/file.go b/pkg/files/file.go new file mode 100644 index 0000000000000000000000000000000000000000..c60ace139992d80e1bf8f41cd28b3bb0e883007a --- /dev/null +++ b/pkg/files/file.go @@ -0,0 +1,90 @@ +package files + +import ( + "fmt" + "io/fs" + "strings" + + "git.perx.ru/perxis/perxis-go/pkg/id" +) + +const ( + TemporaryPrefix = "tmp" +) + +// File - описание файла РІ системе хранения perxis +type File struct { + ID string `mapstructure:"id,omitempty" json:"id"` // Уникальный идентификатор файла РІ хранилище + Name string `mapstructure:"name,omitempty" json:"name" bson:"name,omitempty"` // РРјСЏ файла + Size int `mapstructure:"size,omitempty" json:"size" bson:"size,omitempty"` // Размер файла + MimeType string `mapstructure:"mimeType,omitempty" json:"mimeType" bson:"mimeType,omitempty"` // Mime-type файла + URL string `mapstructure:"url,omitempty" json:"url" bson:"url,omitempty"` // Адрес для загрузки файла + Key string `mapstructure:"key,omitempty" json:"key" bson:"key,omitempty"` // Ключ для хранения файла РІ хранилище + File fs.File `mapstructure:"-" json:"-" bson:"-"` // Файл для загрузки(РёР· файловой системы) +} + +func (f File) Clone() *File { + return &f +} + +func (f File) Temporary() bool { + return strings.HasPrefix(f.ID, TemporaryPrefix) +} + +func (f File) Fetch(i interface{}) interface{} { + p, _ := i.(string) + switch p { + case "id": + return f.ID + case "name": + return f.Name + case "size": + return f.Size + case "mime_type": + return f.MimeType + case "url": + return f.URL + case "key": + return f.Key + default: + panic("unknown parameter") + } + return nil +} + +func NewFile(name, mimeType string, size int, temp bool) *File { + i := id.GenerateNewID() + if temp { + i = fmt.Sprintf("%s%s", TemporaryPrefix, i) + } + return &File{ + ID: i, + Name: name, + Size: size, + MimeType: mimeType, + } +} + +// MultipartUpload - описание загрузки файла +type MultipartUpload struct { + File + UploadID string `json:"upload_id"` // Рдентификатор загрузки хранилища + PartSize int `json:"part_size"` // Размер блока для загрузки + PartURLs []string `json:"part_urls"` // Адреса для загрузки полного файла + Parts []*CompletedPart `json:"parts"` // Рдентификаторы загруженных блоков (S3 ETAGs) +} + +// Upload - описание загрузки файла +type Upload struct { + File + UploadURL string `json:"upload_url"` // Рдентификатор загрузки хранилища +} + +type CompletedPart struct { + Number int `json:"part_number"` + ID string `json:"id"` +} + +func (u MultipartUpload) Clone() *MultipartUpload { + return &u +} diff --git a/pkg/files/mocks/Downloader.go b/pkg/files/mocks/Downloader.go new file mode 100644 index 0000000000000000000000000000000000000000..39bb7567df9916136706ffd1aa2c6634c29e9494 --- /dev/null +++ b/pkg/files/mocks/Downloader.go @@ -0,0 +1,29 @@ +// Code generated by mockery v2.7.4. DO NOT EDIT. + +package mocks + +import ( + io "io" + + files "git.perx.ru/perxis/perxis-go/pkg/files" + mock "github.com/stretchr/testify/mock" +) + +// Downloader is an autogenerated mock type for the Downloader type +type Downloader struct { + mock.Mock +} + +// Download provides a mock function with given fields: dst, file +func (_m *Downloader) Download(dst io.Writer, file *files.File) error { + ret := _m.Called(dst, file) + + var r0 error + if rf, ok := ret.Get(0).(func(io.Writer, *files.File) error); ok { + r0 = rf(dst, file) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/pkg/files/mocks/Files.go b/pkg/files/mocks/Files.go new file mode 100644 index 0000000000000000000000000000000000000000..c6cdc0fa17f4674b98fb4c966d6ad7f87203276b --- /dev/null +++ b/pkg/files/mocks/Files.go @@ -0,0 +1,158 @@ +// Code generated by mockery v2.7.4. DO NOT EDIT. + +package mocks + +import ( + context "context" + + files "git.perx.ru/perxis/perxis-go/pkg/files" + mock "github.com/stretchr/testify/mock" +) + +// Files is an autogenerated mock type for the Files type +type Files struct { + mock.Mock +} + +// AbortUpload provides a mock function with given fields: ctx, upload +func (_m *Files) AbortUpload(ctx context.Context, upload *files.MultipartUpload) error { + ret := _m.Called(ctx, upload) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *files.MultipartUpload) error); ok { + r0 = rf(ctx, upload) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CompleteUpload provides a mock function with given fields: ctx, upload +func (_m *Files) CompleteUpload(ctx context.Context, upload *files.MultipartUpload) (*files.MultipartUpload, error) { + ret := _m.Called(ctx, upload) + + var r0 *files.MultipartUpload + if rf, ok := ret.Get(0).(func(context.Context, *files.MultipartUpload) *files.MultipartUpload); ok { + r0 = rf(ctx, upload) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*files.MultipartUpload) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *files.MultipartUpload) error); ok { + r1 = rf(ctx, upload) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DeleteFile provides a mock function with given fields: ctx, file +func (_m *Files) DeleteFile(ctx context.Context, file *files.File) error { + ret := _m.Called(ctx, file) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *files.File) error); ok { + r0 = rf(ctx, file) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetFile provides a mock function with given fields: ctx, file +func (_m *Files) GetFile(ctx context.Context, file *files.File) (*files.File, error) { + ret := _m.Called(ctx, file) + + var r0 *files.File + if rf, ok := ret.Get(0).(func(context.Context, *files.File) *files.File); ok { + r0 = rf(ctx, file) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*files.File) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *files.File) error); ok { + r1 = rf(ctx, file) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MoveUpload provides a mock function with given fields: ctx, upload +func (_m *Files) MoveUpload(ctx context.Context, upload *files.MultipartUpload) (*files.File, error) { + ret := _m.Called(ctx, upload) + + var r0 *files.File + if rf, ok := ret.Get(0).(func(context.Context, *files.MultipartUpload) *files.File); ok { + r0 = rf(ctx, upload) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*files.File) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *files.MultipartUpload) error); ok { + r1 = rf(ctx, upload) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// StartUpload provides a mock function with given fields: ctx, upload +func (_m *Files) StartUpload(ctx context.Context, upload *files.MultipartUpload) (*files.MultipartUpload, error) { + ret := _m.Called(ctx, upload) + + var r0 *files.MultipartUpload + if rf, ok := ret.Get(0).(func(context.Context, *files.MultipartUpload) *files.MultipartUpload); ok { + r0 = rf(ctx, upload) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*files.MultipartUpload) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *files.MultipartUpload) error); ok { + r1 = rf(ctx, upload) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Upload provides a mock function with given fields: ctx, file +func (_m *Files) Upload(ctx context.Context, file *files.File) (*files.Upload, error) { + ret := _m.Called(ctx, file) + + var r0 *files.Upload + if rf, ok := ret.Get(0).(func(context.Context, *files.File) *files.Upload); ok { + r0 = rf(ctx, file) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*files.Upload) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *files.File) error); ok { + r1 = rf(ctx, file) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/pkg/files/mocks/Uploader.go b/pkg/files/mocks/Uploader.go new file mode 100644 index 0000000000000000000000000000000000000000..66624feef8b3e988ab06d9f73e416fc1afe0fea9 --- /dev/null +++ b/pkg/files/mocks/Uploader.go @@ -0,0 +1,29 @@ +// Code generated by mockery v2.7.4. DO NOT EDIT. + +package mocks + +import ( + io "io" + + files "git.perx.ru/perxis/perxis-go/pkg/files" + mock "github.com/stretchr/testify/mock" +) + +// Uploader is an autogenerated mock type for the Uploader type +type Uploader struct { + mock.Mock +} + +// Upload provides a mock function with given fields: src, upload +func (_m *Uploader) Upload(src io.Reader, upload *files.Upload) error { + ret := _m.Called(src, upload) + + var r0 error + if rf, ok := ret.Get(0).(func(io.Reader, *files.Upload) error); ok { + r0 = rf(src, upload) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/pkg/files/service.go b/pkg/files/service.go new file mode 100644 index 0000000000000000000000000000000000000000..b97af13a01deed33131c6c87a7d5d8f52b70d29a --- /dev/null +++ b/pkg/files/service.go @@ -0,0 +1,36 @@ +package files + +import ( + "context" +) + +// Files - описывает интерфейс файлового сервиса +// @microgen grpc, recovering, middleware +// @protobuf git.perx.ru/perxis/perxis-go/proto/files +// @grpc-addr files.Files +type Files interface { + // StartUpload - инициирует процедуру загрузки файла РІ файловое хранилище. + // Рспользуется клиентским приложением для начала загрузки файла + StartUpload(ctx context.Context, upload *MultipartUpload) (u *MultipartUpload, err error) + + // CompleteUpload - завершает процедуру загрузку файла + CompleteUpload(ctx context.Context, upload *MultipartUpload) (u *MultipartUpload, err error) + + // AbortUpload - прерывает процедуру загрузки файла, РІСЃРµ загруженные части файла удаляются РёС… хранилища + AbortUpload(ctx context.Context, upload *MultipartUpload) (err error) + + // MoveUpload - перемещает загруженный файл РёР· временного расположения РІ постоянное месторасположения. + // После перемещение загрузки хранилище выдает новый идентификатор постоянного файла + MoveUpload(ctx context.Context, upload *MultipartUpload) (file *File, err error) + + // Upload - инициация загрузки файла РІ хранилище. Возвращает объект, содержащий подписанный URL. + // Завершение загрузки файла осуществляется выполнением POST-запроса + Upload(ctx context.Context, file *File) (u *Upload, err error) + + // GetFile - получить объект файла + GetFile(ctx context.Context, file *File) (f *File, err error) + + // DeleteFile - удаляет файл. Если РїСЂРѕРёСЃС…РѕРґРёС‚ удаление оригинала, + // удаляются Рё РІСЃРµ связанные структуры + DeleteFile(ctx context.Context, file *File) (err error) +} diff --git a/pkg/files/transport/client.microgen.go b/pkg/files/transport/client.microgen.go new file mode 100644 index 0000000000000000000000000000000000000000..6320c479808103ebdfef774c51c85fa1ea5f9cfe --- /dev/null +++ b/pkg/files/transport/client.microgen.go @@ -0,0 +1,96 @@ +// Code generated by microgen 0.9.1. DO NOT EDIT. + +package transport + +import ( + "context" + "errors" + + files "git.perx.ru/perxis/perxis-go/pkg/files" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +func (set EndpointsSet) StartUpload(arg0 context.Context, arg1 *files.MultipartUpload) (res0 *files.MultipartUpload, res1 error) { + request := StartUploadRequest{Upload: arg1} + response, res1 := set.StartUploadEndpoint(arg0, &request) + if res1 != nil { + if e, ok := status.FromError(res1); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown { + res1 = errors.New(e.Message()) + } + return + } + return response.(*StartUploadResponse).U, res1 +} + +func (set EndpointsSet) CompleteUpload(arg0 context.Context, arg1 *files.MultipartUpload) (res0 *files.MultipartUpload, res1 error) { + request := CompleteUploadRequest{Upload: arg1} + response, res1 := set.CompleteUploadEndpoint(arg0, &request) + if res1 != nil { + if e, ok := status.FromError(res1); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown { + res1 = errors.New(e.Message()) + } + return + } + return response.(*CompleteUploadResponse).U, res1 +} + +func (set EndpointsSet) AbortUpload(arg0 context.Context, arg1 *files.MultipartUpload) (res0 error) { + request := AbortUploadRequest{Upload: arg1} + _, res0 = set.AbortUploadEndpoint(arg0, &request) + if res0 != nil { + if e, ok := status.FromError(res0); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown { + res0 = errors.New(e.Message()) + } + return + } + return res0 +} + +func (set EndpointsSet) MoveUpload(arg0 context.Context, arg1 *files.MultipartUpload) (res0 *files.File, res1 error) { + request := MoveUploadRequest{Upload: arg1} + response, res1 := set.MoveUploadEndpoint(arg0, &request) + if res1 != nil { + if e, ok := status.FromError(res1); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown { + res1 = errors.New(e.Message()) + } + return + } + return response.(*MoveUploadResponse).File, res1 +} + +func (set EndpointsSet) Upload(arg0 context.Context, arg1 *files.File) (res0 *files.Upload, res1 error) { + request := UploadRequest{File: arg1} + response, res1 := set.UploadEndpoint(arg0, &request) + if res1 != nil { + if e, ok := status.FromError(res1); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown { + res1 = errors.New(e.Message()) + } + return + } + return response.(*UploadResponse).U, res1 +} + +func (set EndpointsSet) GetFile(arg0 context.Context, arg1 *files.File) (res0 *files.File, res1 error) { + request := GetFileRequest{File: arg1} + response, res1 := set.GetFileEndpoint(arg0, &request) + if res1 != nil { + if e, ok := status.FromError(res1); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown { + res1 = errors.New(e.Message()) + } + return + } + return response.(*GetFileResponse).F, res1 +} + +func (set EndpointsSet) DeleteFile(arg0 context.Context, arg1 *files.File) (res0 error) { + request := DeleteFileRequest{File: arg1} + _, res0 = set.DeleteFileEndpoint(arg0, &request) + if res0 != nil { + if e, ok := status.FromError(res0); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown { + res0 = errors.New(e.Message()) + } + return + } + return res0 +} diff --git a/pkg/files/transport/endpoints.microgen.go b/pkg/files/transport/endpoints.microgen.go new file mode 100644 index 0000000000000000000000000000000000000000..96652f6f2ab22546cbdc7375363584173f315069 --- /dev/null +++ b/pkg/files/transport/endpoints.microgen.go @@ -0,0 +1,16 @@ +// Code generated by microgen 0.9.1. DO NOT EDIT. + +package transport + +import endpoint "github.com/go-kit/kit/endpoint" + +// EndpointsSet implements Files API and used for transport purposes. +type EndpointsSet struct { + StartUploadEndpoint endpoint.Endpoint + CompleteUploadEndpoint endpoint.Endpoint + AbortUploadEndpoint endpoint.Endpoint + MoveUploadEndpoint endpoint.Endpoint + UploadEndpoint endpoint.Endpoint + GetFileEndpoint endpoint.Endpoint + DeleteFileEndpoint endpoint.Endpoint +} diff --git a/pkg/files/transport/exchanges.microgen.go b/pkg/files/transport/exchanges.microgen.go new file mode 100644 index 0000000000000000000000000000000000000000..df9a47826c572de96d2cb55bb64aa6730729dd7e --- /dev/null +++ b/pkg/files/transport/exchanges.microgen.go @@ -0,0 +1,54 @@ +// Code generated by microgen 0.9.1. DO NOT EDIT. + +package transport + +import files "git.perx.ru/perxis/perxis-go/pkg/files" + +type ( + StartUploadRequest struct { + Upload *files.MultipartUpload `json:"upload"` + } + StartUploadResponse struct { + U *files.MultipartUpload `json:"u"` + } + + CompleteUploadRequest struct { + Upload *files.MultipartUpload `json:"upload"` + } + CompleteUploadResponse struct { + U *files.MultipartUpload `json:"u"` + } + + AbortUploadRequest struct { + Upload *files.MultipartUpload `json:"upload"` + } + // Formal exchange type, please do not delete. + AbortUploadResponse struct{} + + MoveUploadRequest struct { + Upload *files.MultipartUpload `json:"upload"` + } + MoveUploadResponse struct { + File *files.File `json:"file"` + } + + UploadRequest struct { + File *files.File `json:"file"` + } + UploadResponse struct { + U *files.Upload `json:"u"` + } + + GetFileRequest struct { + File *files.File `json:"file"` + } + GetFileResponse struct { + F *files.File `json:"f"` + } + + DeleteFileRequest struct { + File *files.File `json:"file"` + } + // Formal exchange type, please do not delete. + DeleteFileResponse struct{} +) diff --git a/pkg/files/transport/grpc/client.microgen.go b/pkg/files/transport/grpc/client.microgen.go new file mode 100644 index 0000000000000000000000000000000000000000..0b373ba8af9959a3493ce37dd590113937c5727e --- /dev/null +++ b/pkg/files/transport/grpc/client.microgen.go @@ -0,0 +1,68 @@ +// Code generated by microgen 0.9.1. DO NOT EDIT. + +package transportgrpc + +import ( + transport "git.perx.ru/perxis/perxis-go/pkg/files/transport" + pb "git.perx.ru/perxis/perxis-go/proto/files" + grpckit "github.com/go-kit/kit/transport/grpc" + empty "github.com/golang/protobuf/ptypes/empty" + grpc "google.golang.org/grpc" +) + +func NewGRPCClient(conn *grpc.ClientConn, addr string, opts ...grpckit.ClientOption) transport.EndpointsSet { + if addr == "" { + addr = "files.Files" + } + return transport.EndpointsSet{ + AbortUploadEndpoint: grpckit.NewClient( + conn, addr, "AbortUpload", + _Encode_AbortUpload_Request, + _Decode_AbortUpload_Response, + empty.Empty{}, + opts..., + ).Endpoint(), + CompleteUploadEndpoint: grpckit.NewClient( + conn, addr, "CompleteUpload", + _Encode_CompleteUpload_Request, + _Decode_CompleteUpload_Response, + pb.CompleteUploadResponse{}, + opts..., + ).Endpoint(), + DeleteFileEndpoint: grpckit.NewClient( + conn, addr, "DeleteFile", + _Encode_DeleteFile_Request, + _Decode_DeleteFile_Response, + empty.Empty{}, + opts..., + ).Endpoint(), + GetFileEndpoint: grpckit.NewClient( + conn, addr, "GetFile", + _Encode_GetFile_Request, + _Decode_GetFile_Response, + pb.GetFileResponse{}, + opts..., + ).Endpoint(), + MoveUploadEndpoint: grpckit.NewClient( + conn, addr, "MoveUpload", + _Encode_MoveUpload_Request, + _Decode_MoveUpload_Response, + pb.MoveUploadResponse{}, + opts..., + ).Endpoint(), + StartUploadEndpoint: grpckit.NewClient( + conn, addr, "StartUpload", + _Encode_StartUpload_Request, + _Decode_StartUpload_Response, + pb.StartUploadResponse{}, + opts..., + ).Endpoint(), + UploadEndpoint: grpckit.NewClient( + conn, addr, "Upload", + _Encode_Upload_Request, + _Decode_Upload_Response, + pb.UploadResponse{}, + opts..., + ).Endpoint(), + } +} diff --git a/pkg/files/transport/grpc/protobuf_endpoint_converters.microgen.go b/pkg/files/transport/grpc/protobuf_endpoint_converters.microgen.go new file mode 100644 index 0000000000000000000000000000000000000000..6168318d6d8a278648a92c88981c196f133db53a --- /dev/null +++ b/pkg/files/transport/grpc/protobuf_endpoint_converters.microgen.go @@ -0,0 +1,310 @@ +// Code generated by microgen 0.9.1. DO NOT EDIT. + +// Please, do not change functions names! +package transportgrpc + +import ( + "context" + "errors" + + "git.perx.ru/perxis/perxis-go/pkg/files" + "git.perx.ru/perxis/perxis-go/pkg/files/transport" + pb "git.perx.ru/perxis/perxis-go/proto/files" + empty "github.com/golang/protobuf/ptypes/empty" +) + +func _Encode_StartUpload_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil StartUploadRequest") + } + req := request.(*transport.StartUploadRequest) + reqUpload, err := PtrMultipartUploadToProto(req.Upload) + if err != nil { + return nil, err + } + return &pb.StartUploadRequest{Upload: reqUpload}, nil +} + +func _Encode_CompleteUpload_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil CompleteUploadRequest") + } + req := request.(*transport.CompleteUploadRequest) + reqUpload, err := PtrMultipartUploadToProto(req.Upload) + if err != nil { + return nil, err + } + return &pb.CompleteUploadRequest{Upload: reqUpload}, nil +} + +func _Encode_AbortUpload_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil AbortUploadRequest") + } + req := request.(*transport.AbortUploadRequest) + reqUpload, err := PtrMultipartUploadToProto(req.Upload) + if err != nil { + return nil, err + } + return &pb.AbortUploadRequest{Upload: reqUpload}, nil +} + +func _Encode_MoveUpload_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil MoveUploadRequest") + } + req := request.(*transport.MoveUploadRequest) + reqUpload, err := PtrMultipartUploadToProto(req.Upload) + if err != nil { + return nil, err + } + return &pb.MoveUploadRequest{Upload: reqUpload}, nil +} + +func _Encode_DeleteFile_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil DeleteFileRequest") + } + req := request.(*transport.DeleteFileRequest) + reqFile, err := PtrFileToProto(req.File) + if err != nil { + return nil, err + } + return &pb.DeleteFileRequest{File: reqFile}, nil +} + +func _Encode_StartUpload_Response(ctx context.Context, response interface{}) (interface{}, error) { + if response == nil { + return nil, errors.New("nil StartUploadResponse") + } + resp := response.(*transport.StartUploadResponse) + respU, err := PtrMultipartUploadToProto(resp.U) + if err != nil { + return nil, err + } + return &pb.StartUploadResponse{Upload: respU}, nil +} + +func _Encode_CompleteUpload_Response(ctx context.Context, response interface{}) (interface{}, error) { + if response == nil { + return nil, errors.New("nil CompleteUploadResponse") + } + resp := response.(*transport.CompleteUploadResponse) + respU, err := PtrMultipartUploadToProto(resp.U) + if err != nil { + return nil, err + } + return &pb.CompleteUploadResponse{Upload: respU}, nil +} + +func _Encode_AbortUpload_Response(ctx context.Context, response interface{}) (interface{}, error) { + return &empty.Empty{}, nil +} + +func _Encode_MoveUpload_Response(ctx context.Context, response interface{}) (interface{}, error) { + if response == nil { + return nil, errors.New("nil MoveUploadResponse") + } + resp := response.(*transport.MoveUploadResponse) + respFile, err := PtrFileToProto(resp.File) + if err != nil { + return nil, err + } + return &pb.MoveUploadResponse{File: respFile}, nil +} + +func _Encode_DeleteFile_Response(ctx context.Context, response interface{}) (interface{}, error) { + return &empty.Empty{}, nil +} + +func _Decode_StartUpload_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil StartUploadRequest") + } + req := request.(*pb.StartUploadRequest) + reqUpload, err := ProtoToPtrMultipartUpload(req.Upload) + if err != nil { + return nil, err + } + return &transport.StartUploadRequest{Upload: reqUpload}, nil +} + +func _Decode_CompleteUpload_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil CompleteUploadRequest") + } + req := request.(*pb.CompleteUploadRequest) + reqUpload, err := ProtoToPtrMultipartUpload(req.Upload) + if err != nil { + return nil, err + } + return &transport.CompleteUploadRequest{Upload: reqUpload}, nil +} + +func _Decode_AbortUpload_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil AbortUploadRequest") + } + req := request.(*pb.AbortUploadRequest) + reqUpload, err := ProtoToPtrMultipartUpload(req.Upload) + if err != nil { + return nil, err + } + return &transport.AbortUploadRequest{Upload: reqUpload}, nil +} + +func _Decode_MoveUpload_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil MoveUploadRequest") + } + req := request.(*pb.MoveUploadRequest) + reqUpload, err := ProtoToPtrMultipartUpload(req.Upload) + if err != nil { + return nil, err + } + return &transport.MoveUploadRequest{Upload: reqUpload}, nil +} + +func _Decode_DeleteFile_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil DeleteFileRequest") + } + req := request.(*pb.DeleteFileRequest) + reqFile, err := ProtoToPtrFile(req.File) + if err != nil { + return nil, err + } + return &transport.DeleteFileRequest{File: reqFile}, nil +} + +func _Decode_StartUpload_Response(ctx context.Context, response interface{}) (interface{}, error) { + if response == nil { + return nil, errors.New("nil StartUploadResponse") + } + resp := response.(*pb.StartUploadResponse) + respU, err := ProtoToPtrMultipartUpload(resp.Upload) + if err != nil { + return nil, err + } + return &transport.StartUploadResponse{U: respU}, nil +} + +func _Decode_CompleteUpload_Response(ctx context.Context, response interface{}) (interface{}, error) { + if response == nil { + return nil, errors.New("nil CompleteUploadResponse") + } + resp := response.(*pb.CompleteUploadResponse) + respU, err := ProtoToPtrMultipartUpload(resp.Upload) + if err != nil { + return nil, err + } + return &transport.CompleteUploadResponse{U: respU}, nil +} + +func _Decode_AbortUpload_Response(ctx context.Context, response interface{}) (interface{}, error) { + return &empty.Empty{}, nil +} + +func _Decode_MoveUpload_Response(ctx context.Context, response interface{}) (interface{}, error) { + if response == nil { + return nil, errors.New("nil MoveUploadResponse") + } + resp := response.(*pb.MoveUploadResponse) + respFile, err := ProtoToPtrFile(resp.File) + if err != nil { + return nil, err + } + return &transport.MoveUploadResponse{File: respFile}, nil +} + +func _Decode_DeleteFile_Response(ctx context.Context, response interface{}) (interface{}, error) { + return &empty.Empty{}, nil +} + +func _Encode_GetFile_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil GetFileRequest") + } + req := request.(*transport.GetFileRequest) + return &pb.GetFileRequest{Id: req.File.ID}, nil +} + +func _Encode_GetFile_Response(ctx context.Context, response interface{}) (interface{}, error) { + if response == nil { + return nil, errors.New("nil GetFileResponse") + } + resp := response.(*transport.GetFileResponse) + respF, err := PtrFileToProto(resp.F) + if err != nil { + return nil, err + } + return &pb.GetFileResponse{File: respF}, nil +} + +func _Decode_GetFile_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil GetFileRequest") + } + req := request.(*pb.GetFileRequest) + return &transport.GetFileRequest{File: &files.File{ID: req.Id}}, nil +} + +func _Decode_GetFile_Response(ctx context.Context, response interface{}) (interface{}, error) { + if response == nil { + return nil, errors.New("nil GetFileResponse") + } + resp := response.(*pb.GetFileResponse) + respF, err := ProtoToPtrFile(resp.File) + if err != nil { + return nil, err + } + return &transport.GetFileResponse{F: respF}, nil +} + +func _Encode_Upload_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil UploadRequest") + } + req := request.(*transport.UploadRequest) + reqFile, err := PtrFileToProto(req.File) + if err != nil { + return nil, err + } + return &pb.UploadRequest{File: reqFile}, nil +} + +func _Encode_Upload_Response(ctx context.Context, response interface{}) (interface{}, error) { + if response == nil { + return nil, errors.New("nil UploadResponse") + } + resp := response.(*transport.UploadResponse) + respFile, err := PtrUploadToProto(resp.U) + if err != nil { + return nil, err + } + return &pb.UploadResponse{Upload: respFile}, nil +} + +func _Decode_Upload_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil UploadRequest") + } + req := request.(*pb.UploadRequest) + reqFile, err := ProtoToPtrFile(req.File) + if err != nil { + return nil, err + } + return &transport.UploadRequest{File: reqFile}, nil +} + +func _Decode_Upload_Response(ctx context.Context, response interface{}) (interface{}, error) { + if response == nil { + return nil, errors.New("nil UploadResponse") + } + resp := response.(*pb.UploadResponse) + respUpload, err := ProtoToPtrUpload(resp.Upload) + if err != nil { + return nil, err + } + return &transport.UploadResponse{U: respUpload}, nil +} diff --git a/pkg/files/transport/grpc/protobuf_type_converters.microgen.go b/pkg/files/transport/grpc/protobuf_type_converters.microgen.go new file mode 100644 index 0000000000000000000000000000000000000000..1240f9fc2e59251a6792fd04214455c6c75bb050 --- /dev/null +++ b/pkg/files/transport/grpc/protobuf_type_converters.microgen.go @@ -0,0 +1,118 @@ +// Code generated by microgen 0.9.1. DO NOT EDIT. + +// It is better for you if you do not change functions names! +// This files will never be overwritten. +package transportgrpc + +import ( + "git.perx.ru/perxis/perxis-go/pkg/files" + pb "git.perx.ru/perxis/perxis-go/proto/files" +) + +func PtrMultipartUploadToProto(upload *files.MultipartUpload) (*pb.MultipartUpload, error) { + if upload == nil { + return nil, nil + } + pbUpload := &pb.MultipartUpload{ + UploadId: upload.UploadID, + PartSize: int32(upload.PartSize), + PartUrls: upload.PartURLs, + } + pbUpload.Parts = make([]*pb.CompletedPart, 0, len(upload.Parts)) + for _, part := range upload.Parts { + pbUpload.Parts = append(pbUpload.Parts, &pb.CompletedPart{ + Number: int32(part.Number), + Id: part.ID, + }) + } + pbFile, err := PtrFileToProto(&upload.File) + if err != nil { + return nil, err + } + pbUpload.File = pbFile + return pbUpload, nil +} + +func ProtoToPtrMultipartUpload(protoUpload *pb.MultipartUpload) (*files.MultipartUpload, error) { + if protoUpload == nil { + return nil, nil + } + upload := &files.MultipartUpload{ + UploadID: protoUpload.UploadId, + PartSize: int(protoUpload.PartSize), + PartURLs: protoUpload.PartUrls, + } + upload.Parts = make([]*files.CompletedPart, 0, len(protoUpload.Parts)) + for _, part := range protoUpload.Parts { + upload.Parts = append(upload.Parts, &files.CompletedPart{ + Number: int(part.Number), + ID: part.Id, + }) + } + file, err := ProtoToPtrFile(protoUpload.File) + if err != nil { + return nil, err + } + if file != nil { + upload.File = *file + } + return upload, nil +} + +func PtrUploadToProto(upload *files.Upload) (*pb.Upload, error) { + if upload == nil { + return nil, nil + } + pbUpload := &pb.Upload{ + UploadUrl: upload.UploadURL, + } + pbFile, err := PtrFileToProto(&upload.File) + if err != nil { + return nil, err + } + pbUpload.File = pbFile + return pbUpload, nil +} + +func ProtoToPtrUpload(protoUpload *pb.Upload) (*files.Upload, error) { + if protoUpload == nil { + return nil, nil + } + upload := &files.Upload{ + UploadURL: protoUpload.UploadUrl, + } + file, err := ProtoToPtrFile(protoUpload.File) + if err != nil { + return nil, err + } + upload.File = *file + return upload, nil +} + +func PtrFileToProto(file *files.File) (*pb.File, error) { + if file == nil { + return nil, nil + } + pbFile := &pb.File{ + Id: file.ID, + Name: file.Name, + Size: int32(file.Size), + MimeType: file.MimeType, + Url: file.URL, + } + return pbFile, nil +} + +func ProtoToPtrFile(protoFile *pb.File) (*files.File, error) { + if protoFile == nil { + return nil, nil + } + file := &files.File{ + ID: protoFile.Id, + Name: protoFile.Name, + Size: int(protoFile.Size), + MimeType: protoFile.MimeType, + URL: protoFile.Url, + } + return file, nil +} diff --git a/pkg/files/transport/grpc/server.microgen.go b/pkg/files/transport/grpc/server.microgen.go new file mode 100644 index 0000000000000000000000000000000000000000..c4fb6cd1f1751bb3beb69696ce81635369b43085 --- /dev/null +++ b/pkg/files/transport/grpc/server.microgen.go @@ -0,0 +1,127 @@ +// Code generated by microgen 0.9.1. DO NOT EDIT. + +// DO NOT EDIT. +package transportgrpc + +import ( + transport "git.perx.ru/perxis/perxis-go/pkg/files/transport" + pb "git.perx.ru/perxis/perxis-go/proto/files" + grpc "github.com/go-kit/kit/transport/grpc" + empty "github.com/golang/protobuf/ptypes/empty" + context "golang.org/x/net/context" +) + +type filesServer struct { + startUpload grpc.Handler + completeUpload grpc.Handler + abortUpload grpc.Handler + moveUpload grpc.Handler + upload grpc.Handler + getFile grpc.Handler + deleteFile grpc.Handler + + pb.UnimplementedFilesServer +} + +func NewGRPCServer(endpoints *transport.EndpointsSet, opts ...grpc.ServerOption) pb.FilesServer { + return &filesServer{ + abortUpload: grpc.NewServer( + endpoints.AbortUploadEndpoint, + _Decode_AbortUpload_Request, + _Encode_AbortUpload_Response, + opts..., + ), + completeUpload: grpc.NewServer( + endpoints.CompleteUploadEndpoint, + _Decode_CompleteUpload_Request, + _Encode_CompleteUpload_Response, + opts..., + ), + deleteFile: grpc.NewServer( + endpoints.DeleteFileEndpoint, + _Decode_DeleteFile_Request, + _Encode_DeleteFile_Response, + opts..., + ), + getFile: grpc.NewServer( + endpoints.GetFileEndpoint, + _Decode_GetFile_Request, + _Encode_GetFile_Response, + opts..., + ), + moveUpload: grpc.NewServer( + endpoints.MoveUploadEndpoint, + _Decode_MoveUpload_Request, + _Encode_MoveUpload_Response, + opts..., + ), + startUpload: grpc.NewServer( + endpoints.StartUploadEndpoint, + _Decode_StartUpload_Request, + _Encode_StartUpload_Response, + opts..., + ), + upload: grpc.NewServer( + endpoints.UploadEndpoint, + _Decode_Upload_Request, + _Encode_Upload_Response, + opts..., + ), + } +} + +func (S *filesServer) StartUpload(ctx context.Context, req *pb.StartUploadRequest) (*pb.StartUploadResponse, error) { + _, resp, err := S.startUpload.ServeGRPC(ctx, req) + if err != nil { + return nil, err + } + return resp.(*pb.StartUploadResponse), nil +} + +func (S *filesServer) CompleteUpload(ctx context.Context, req *pb.CompleteUploadRequest) (*pb.CompleteUploadResponse, error) { + _, resp, err := S.completeUpload.ServeGRPC(ctx, req) + if err != nil { + return nil, err + } + return resp.(*pb.CompleteUploadResponse), nil +} + +func (S *filesServer) AbortUpload(ctx context.Context, req *pb.AbortUploadRequest) (*empty.Empty, error) { + _, resp, err := S.abortUpload.ServeGRPC(ctx, req) + if err != nil { + return nil, err + } + return resp.(*empty.Empty), nil +} + +func (S *filesServer) MoveUpload(ctx context.Context, req *pb.MoveUploadRequest) (*pb.MoveUploadResponse, error) { + _, resp, err := S.moveUpload.ServeGRPC(ctx, req) + if err != nil { + return nil, err + } + return resp.(*pb.MoveUploadResponse), nil +} + +func (S *filesServer) Upload(ctx context.Context, req *pb.UploadRequest) (*pb.UploadResponse, error) { + _, resp, err := S.upload.ServeGRPC(ctx, req) + if err != nil { + return nil, err + } + return resp.(*pb.UploadResponse), nil +} + +func (S *filesServer) GetFile(ctx context.Context, req *pb.GetFileRequest) (*pb.GetFileResponse, error) { + _, resp, err := S.getFile.ServeGRPC(ctx, req) + if err != nil { + return nil, err + } + return resp.(*pb.GetFileResponse), nil +} + +func (S *filesServer) DeleteFile(ctx context.Context, req *pb.DeleteFileRequest) (*empty.Empty, error) { + _, resp, err := S.deleteFile.ServeGRPC(ctx, req) + if err != nil { + return nil, err + } + return resp.(*empty.Empty), nil +} diff --git a/pkg/files/transport/server.microgen.go b/pkg/files/transport/server.microgen.go new file mode 100644 index 0000000000000000000000000000000000000000..7cd25b06d7810e0bcd7371224264f551d6ba4832 --- /dev/null +++ b/pkg/files/transport/server.microgen.go @@ -0,0 +1,78 @@ +// Code generated by microgen 0.9.1. DO NOT EDIT. + +package transport + +import ( + "context" + + files "git.perx.ru/perxis/perxis-go/pkg/files" + endpoint "github.com/go-kit/kit/endpoint" +) + +func Endpoints(svc files.Files) EndpointsSet { + return EndpointsSet{ + AbortUploadEndpoint: AbortUploadEndpoint(svc), + CompleteUploadEndpoint: CompleteUploadEndpoint(svc), + DeleteFileEndpoint: DeleteFileEndpoint(svc), + GetFileEndpoint: GetFileEndpoint(svc), + MoveUploadEndpoint: MoveUploadEndpoint(svc), + StartUploadEndpoint: StartUploadEndpoint(svc), + UploadEndpoint: UploadEndpoint(svc), + } +} + +func StartUploadEndpoint(svc files.Files) endpoint.Endpoint { + return func(arg0 context.Context, request interface{}) (interface{}, error) { + req := request.(*StartUploadRequest) + res0, res1 := svc.StartUpload(arg0, req.Upload) + return &StartUploadResponse{U: res0}, res1 + } +} + +func CompleteUploadEndpoint(svc files.Files) endpoint.Endpoint { + return func(arg0 context.Context, request interface{}) (interface{}, error) { + req := request.(*CompleteUploadRequest) + res0, res1 := svc.CompleteUpload(arg0, req.Upload) + return &CompleteUploadResponse{U: res0}, res1 + } +} + +func AbortUploadEndpoint(svc files.Files) endpoint.Endpoint { + return func(arg0 context.Context, request interface{}) (interface{}, error) { + req := request.(*AbortUploadRequest) + res0 := svc.AbortUpload(arg0, req.Upload) + return &AbortUploadResponse{}, res0 + } +} + +func MoveUploadEndpoint(svc files.Files) endpoint.Endpoint { + return func(arg0 context.Context, request interface{}) (interface{}, error) { + req := request.(*MoveUploadRequest) + res0, res1 := svc.MoveUpload(arg0, req.Upload) + return &MoveUploadResponse{File: res0}, res1 + } +} + +func UploadEndpoint(svc files.Files) endpoint.Endpoint { + return func(arg0 context.Context, request interface{}) (interface{}, error) { + req := request.(*UploadRequest) + res0, res1 := svc.Upload(arg0, req.File) + return &UploadResponse{U: res0}, res1 + } +} + +func GetFileEndpoint(svc files.Files) endpoint.Endpoint { + return func(arg0 context.Context, request interface{}) (interface{}, error) { + req := request.(*GetFileRequest) + res0, res1 := svc.GetFile(arg0, req.File) + return &GetFileResponse{F: res0}, res1 + } +} + +func DeleteFileEndpoint(svc files.Files) endpoint.Endpoint { + return func(arg0 context.Context, request interface{}) (interface{}, error) { + req := request.(*DeleteFileRequest) + res0 := svc.DeleteFile(arg0, req.File) + return &DeleteFileResponse{}, res0 + } +} diff --git a/pkg/files/uploader.go b/pkg/files/uploader.go new file mode 100644 index 0000000000000000000000000000000000000000..5b03f62a8007c85a64bdaa3ce8961f4d28ceaef3 --- /dev/null +++ b/pkg/files/uploader.go @@ -0,0 +1,42 @@ +package files + +import ( + "errors" + "fmt" + "io" + "net/http" + "net/url" +) + +type Uploader interface { + Upload(src io.Reader, upload *Upload) error +} + +type uploader struct{} + +func NewUploader() Uploader { + return &uploader{} +} + +func (u *uploader) Upload(src io.Reader, upload *Upload) error { + + req, err := http.NewRequest(http.MethodPut, upload.UploadURL, src) + if err != nil { + return err + } + + req.ContentLength = int64(upload.Size) + req.Header.Set("Content-Disposition", fmt.Sprintf("attachment; filename*=UTF-8''%s;", url.PathEscape(upload.Name))) + req.Header.Set("Content-Type", upload.MimeType) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + + if resp.StatusCode != http.StatusOK { + return errors.New("upload request failed: " + resp.Status) + } + + return nil +} diff --git a/pkg/urlsigner/urlsigner.go b/pkg/urlsigner/urlsigner.go new file mode 100644 index 0000000000000000000000000000000000000000..c5535db0f3ae4f6a2deba7fbb0a1504af6aacf75 --- /dev/null +++ b/pkg/urlsigner/urlsigner.go @@ -0,0 +1,126 @@ +package urlsigner + +import ( + "bytes" + "crypto/sha256" + "encoding/base64" + "net/url" + "strconv" + "strings" + "time" + + "git.perx.ru/perxis/perxis-go/pkg/data" +) + +type URLSigner interface { + Sign(url *url.URL) *url.URL + Check(url *url.URL) bool +} + +const ( + defaultSignatureExpire = 15 * time.Minute + defaultQueryKey = "sign" + + separator = "|" + saltSize = 16 +) + +type urlSigner struct { + secret string + expirationTime time.Duration + queryKey string + params []string +} + +func NewURLSigner(secret string, expirationTime time.Duration, queryKey string, params ...string) URLSigner { + if len(params) == 0 { + params = make([]string, 0) + } + if expirationTime == 0 { + expirationTime = defaultSignatureExpire + } + if queryKey == "" { + queryKey = defaultQueryKey + } + return &urlSigner{ + secret: secret, + expirationTime: expirationTime, + queryKey: queryKey, + params: params, + } +} + +func (s *urlSigner) Sign(u *url.URL) *url.URL { + + q := u.Query() + + h := sha256.New() + salt := data.GenerateRandomString(saltSize) + for _, p := range s.params { + if vv := q[p]; len(vv) > 0 { + for _, v := range vv { + h.Write([]byte(v)) + } + } + } + + h.Write([]byte(u.Path)) + h.Write([]byte(s.expirationTime.String())) + h.Write([]byte(s.secret)) + h.Write([]byte(salt)) + + expTime := time.Now().Add(s.expirationTime).Unix() + res := strings.Join([]string{strconv.FormatInt(expTime, 16), salt, string(h.Sum(nil))}, separator) + + q.Set(s.queryKey, base64.URLEncoding.EncodeToString([]byte(res))) + u.RawQuery = q.Encode() + return u +} + +func (s *urlSigner) Check(u *url.URL) bool { + + q := u.Query() + sign := q.Get(s.queryKey) + if sign == "" { + return false + } + + b, err := base64.URLEncoding.DecodeString(sign) + if err != nil { + return false + } + + m := bytes.Split(b, []byte(separator)) + if len(m) < 3 { + return false + } + + expTime, err := strconv.ParseInt(string(m[0]), 16, 64) + if err != nil || time.Now().Unix() > expTime { + return false + } + + salt := m[1] + var hash []byte + for i := 2; i < len(m); i++ { + hash = append(hash, m[i]...) + if len(m) > i+1 { + hash = append(hash, []byte(separator)...) + } + } + + h := sha256.New() + for _, p := range s.params { + if vv := q[p]; len(vv) > 0 { + for _, v := range vv { + h.Write([]byte(v)) + } + } + } + h.Write([]byte(u.Path)) + h.Write([]byte(s.expirationTime.String())) + h.Write([]byte(s.secret)) + h.Write(salt) + + return bytes.Equal(hash, h.Sum(nil)) +} diff --git a/pkg/urlsigner/urlsigner_test.go b/pkg/urlsigner/urlsigner_test.go new file mode 100644 index 0000000000000000000000000000000000000000..d4a004bfcfe27cddc8d7aae99c6462669e611e7f --- /dev/null +++ b/pkg/urlsigner/urlsigner_test.go @@ -0,0 +1,92 @@ +package urlsigner + +import ( + "net/url" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +const secret = "secret_key" + +func TestSigner(t *testing.T) { + + t.Run("Not Signed", func(t *testing.T) { + u, _ := url.Parse("http://example.com/path") + + signer := NewURLSigner(secret, time.Minute, "") + require.False(t, signer.Check(u)) + }) + t.Run("Simple", func(t *testing.T) { + u, _ := url.Parse("http://example.com/path") + + signer := NewURLSigner(secret, time.Minute, "") + u = signer.Sign(u) + require.True(t, signer.Check(u)) + }) + t.Run("Custom Query Key", func(t *testing.T) { + u, _ := url.Parse("http://example.com/path") + + signer := NewURLSigner(secret, time.Minute, "custom") + u = signer.Sign(u) + require.True(t, signer.Check(u)) + }) + t.Run("URL Expired", func(t *testing.T) { + u, _ := url.Parse("http://example.com/path") + + signer := NewURLSigner(secret, time.Millisecond, "") + u = signer.Sign(u) + time.Sleep(time.Second) + require.False(t, signer.Check(u)) + }) + + t.Run("Required Params", func(t *testing.T) { + t.Run("Correct", func(t *testing.T) { + u, _ := url.Parse("http://example.com/path?param1=1¶m2=2") + signer := NewURLSigner(secret, time.Minute, "", "param1", "param2") + u = signer.Sign(u) + require.True(t, signer.Check(u)) + }) + t.Run("One Param Empty", func(t *testing.T) { + u, _ := url.Parse("http://example.com/path?param1=1") + signer := NewURLSigner(secret, time.Minute, "", "param1", "param2") + u = signer.Sign(u) + require.True(t, signer.Check(u)) + }) + t.Run("Exchanged Values", func(t *testing.T) { + u, _ := url.Parse("http://example.com/path?param1=1¶m2=2") + signer := NewURLSigner(secret, time.Minute, "", "param1", "param2") + u = signer.Sign(u) + + q := u.Query() + q.Set("param1", "2") + q.Set("param2", "1") + u.RawQuery = q.Encode() + + require.False(t, signer.Check(u)) + }) + }) + t.Run("Extra params", func(t *testing.T) { + u, _ := url.Parse("http://example.com/path?param1=1") + signer := NewURLSigner(secret, time.Minute, "", "param1") + u = signer.Sign(u) + + q := u.Query() + q.Set("extra", "100") + u.RawQuery = q.Encode() + + require.True(t, signer.Check(u)) + }) + t.Run("Array Param Value", func(t *testing.T) { + u, _ := url.Parse("http://example.com/path?param1=1¶m1=2¶m1=3") + signer := NewURLSigner(secret, time.Minute, "", "param1") + u = signer.Sign(u) + + q := u.Query() + q.Set("param1", "1") + u.RawQuery = q.Encode() + + require.False(t, signer.Check(u)) + }) +}