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&param2=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&param2=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&param1=2&param1=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))
+	})
+}