hooks: Allow pre-create to change upload ID, meta data and storage details (#962)

* hooks: Allow pre-create to change upload ID, meta data and storage details

* fixup! hooks: Allow pre-create to change upload ID, meta data and storage details

* hooks: Add support for ChangeFileInfo in gRPC
This commit is contained in:
Marius 2023-06-20 11:32:51 +02:00 committed by GitHub
parent bbf9e6011d
commit 71765b61e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 973 additions and 505 deletions

View File

@ -18,10 +18,10 @@ func hookTypeInSlice(a hooks.HookType, list []hooks.HookType) bool {
return false
}
func preCreateCallback(event handler.HookEvent) (handler.HTTPResponse, error) {
func preCreateCallback(event handler.HookEvent) (handler.HTTPResponse, handler.FileInfoChanges, error) {
ok, hookRes, err := invokeHookSync(hooks.HookPreCreate, event)
if !ok || err != nil {
return handler.HTTPResponse{}, err
return handler.HTTPResponse{}, handler.FileInfoChanges{}, err
}
httpRes := hookRes.HTTPResponse
@ -32,10 +32,12 @@ func preCreateCallback(event handler.HookEvent) (handler.HTTPResponse, error) {
err := handler.ErrUploadRejectedByServer
err.HTTPResponse = err.HTTPResponse.MergeWith(httpRes)
return handler.HTTPResponse{}, err
return handler.HTTPResponse{}, handler.FileInfoChanges{}, err
}
return httpRes, nil
// Pass any changes regarding file info from the hook to the handler.
changes := hookRes.ChangeFileInfo
return httpRes, changes, nil
}
func preFinishCallback(event handler.HookEvent) (handler.HTTPResponse, error) {

View File

@ -94,5 +94,12 @@ func unmarshal(res *pb.HookResponse) (hookRes HookResponse) {
hookRes.HTTPResponse.Body = httpRes.Body
}
changes := res.ChangeFileInfo
if changes != nil {
hookRes.ChangeFileInfo.ID = changes.Id
hookRes.ChangeFileInfo.MetaData = changes.MetaData
hookRes.ChangeFileInfo.Storage = changes.Storage
}
return hookRes
}

View File

@ -48,6 +48,13 @@ type HookResponse struct {
// to the client.
RejectUpload bool
// ChangeFileInfo can be set to change selected properties of an upload before
// it has been created. See the handler.FileInfoChanges type for more details.
// Changes are applied on a per-property basis, meaning that specifying just
// one property leaves all others unchanged.
// This value is only respected for pre-create hooks.
ChangeFileInfo handler.FileInfoChanges
// StopUpload will cause the upload to be stopped during a PATCH request.
// This value is only respected for post-receive hooks. For other hooks,
// it is ignored. Use the HTTPResponse field to send details about the stop

View File

@ -1,13 +1,15 @@
// If this file gets changed, you must recompile the generate package in pkg/proto.
// To do this, install the Go protobuf toolchain as mentioned in
// https://github.com/golang/protobuf#installation.
// Then use following command to recompile it with gRPC support:
// protoc --go_out=plugins=grpc:../../../../../pkg/proto/ v2/hook.proto
// https://grpc.io/docs/languages/go/quickstart/#prerequisites.
// Then use following command from the repository's root to recompile it with gRPC support:
// protoc --go-grpc_out=./pkg/ --go_out=./pkg/ ./cmd/tusd/cli/hooks/proto/v2/hook.proto
// In addition, it may be necessary to update the protobuf or gRPC dependencies as well.
syntax = "proto3";
package v2;
option go_package = "proto/v2";
// HookRequest contains the information about the hook type, the involved upload,
// and causing HTTP request.
message HookRequest {
@ -56,6 +58,35 @@ message FileInfo {
map <string, string> storage = 9;
}
// FileInfoChanges collects changes the should be made to a FileInfo object. This
// can be done using the PreUploadCreateCallback to modify certain properties before
// an upload is created. Properties which should not be modified (e.g. Size or Offset)
// are intentionally left out here.
message FileInfoChanges {
// If ID is not empty, it will be passed to the data store, allowing
// hooks to influence the upload ID. Be aware that a data store is not required to
// respect a pre-defined upload ID and might overwrite or modify it. However,
// all data stores in the github.com/tus/tusd package do respect pre-defined IDs.
string id = 1;
// If MetaData is not nil, it replaces the entire user-defined meta data from
// the upload creation request. You can add custom meta data fields this way
// or ensure that only certain fields from the user-defined meta data are saved.
// If you want to retain only specific entries from the user-defined meta data, you must
// manually copy them into this MetaData field.
// If you do not want to store any meta data, set this field to an empty map (`MetaData{}`).
// If you want to keep the entire user-defined meta data, set this field to nil.
map <string, string> metaData = 2;
// If Storage is not nil, it is passed to the data store to allow for minor adjustments
// to the upload storage (e.g. destination file name). The details are specific for each
// data store and should be looked up in their respective documentation.
// Please be aware that this behavior is currently not supported by any data store in
// the github.com/tus/tusd package.
map <string, string> storage = 3;
}
// HTTPRequest contains basic details of an incoming HTTP request.
message HTTPRequest {
// Method is the HTTP method, e.g. POST or PATCH.
@ -87,6 +118,13 @@ message HookResponse {
// to the client.
bool rejectUpload = 2;
// ChangeFileInfo can be set to change selected properties of an upload before
// it has been created. See the handler.FileInfoChanges type for more details.
// Changes are applied on a per-property basis, meaning that specifying just
// one property leaves all others unchanged.
// This value is only respected for pre-create hooks.
FileInfoChanges changeFileInfo = 4;
// StopUpload will cause the upload to be stopped during a PATCH request.
// This value is only respected for post-receive hooks. For other hooks,
// it is ignored. Use the HTTPResponse field to send details about the stop
@ -104,7 +142,6 @@ message HTTPResponse {
string body = 3;
}
// The hook service definition.
service HookHandler {
// InvokeHook is invoked for every hook that is executed. HookRequest contains the

View File

@ -1,2 +1,2 @@
hook_pb2.py:
python -m grpc_tools.protoc --proto_path=../../../cmd/tusd/cli/hooks/proto/v2/ hook.proto --python_out=. --grpc_python_out=.
hook_pb2.py: ../../../cmd/tusd/cli/hooks/proto/v2/hook.proto
python3 -m grpc_tools.protoc --proto_path=../../../cmd/tusd/cli/hooks/proto/v2/ hook.proto --python_out=. --grpc_python_out=.

View File

@ -2,10 +2,9 @@
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: hook.proto
"""Generated protocol buffer code."""
from google.protobuf.internal import builder as _builder
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import message as _message
from google.protobuf import reflection as _reflection
from google.protobuf import symbol_database as _symbol_database
# @@protoc_insertion_point(imports)
@ -14,95 +13,10 @@ _sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\nhook.proto\x12\x02v2\"5\n\x0bHookRequest\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x18\n\x05\x65vent\x18\x02 \x01(\x0b\x32\t.v2.Event\"K\n\x05\x45vent\x12\x1c\n\x06upload\x18\x01 \x01(\x0b\x32\x0c.v2.FileInfo\x12$\n\x0bhttpRequest\x18\x02 \x01(\x0b\x32\x0f.v2.HTTPRequest\"\xc3\x02\n\x08\x46ileInfo\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04size\x18\x02 \x01(\x03\x12\x16\n\x0esizeIsDeferred\x18\x03 \x01(\x08\x12\x0e\n\x06offset\x18\x04 \x01(\x03\x12,\n\x08metaData\x18\x05 \x03(\x0b\x32\x1a.v2.FileInfo.MetaDataEntry\x12\x11\n\tisPartial\x18\x06 \x01(\x08\x12\x0f\n\x07isFinal\x18\x07 \x01(\x08\x12\x16\n\x0epartialUploads\x18\x08 \x03(\t\x12*\n\x07storage\x18\t \x03(\x0b\x32\x19.v2.FileInfo.StorageEntry\x1a/\n\rMetaDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1a.\n\x0cStorageEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x9a\x01\n\x0bHTTPRequest\x12\x0e\n\x06method\x18\x01 \x01(\t\x12\x0b\n\x03uri\x18\x02 \x01(\t\x12\x12\n\nremoteAddr\x18\x03 \x01(\t\x12+\n\x06header\x18\x04 \x03(\x0b\x32\x1b.v2.HTTPRequest.HeaderEntry\x1a-\n\x0bHeaderEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"`\n\x0cHookResponse\x12&\n\x0chttpResponse\x18\x01 \x01(\x0b\x32\x10.v2.HTTPResponse\x12\x14\n\x0crejectUpload\x18\x02 \x01(\x08\x12\x12\n\nstopUpload\x18\x03 \x01(\x08\"\x90\x01\n\x0cHTTPResponse\x12\x12\n\nstatusCode\x18\x01 \x01(\x03\x12.\n\x07headers\x18\x02 \x03(\x0b\x32\x1d.v2.HTTPResponse.HeadersEntry\x12\x0c\n\x04\x62ody\x18\x03 \x01(\t\x1a.\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x32@\n\x0bHookHandler\x12\x31\n\nInvokeHook\x12\x0f.v2.HookRequest\x1a\x10.v2.HookResponse\"\x00\x62\x06proto3')
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\nhook.proto\x12\x02v2\"5\n\x0bHookRequest\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x18\n\x05\x65vent\x18\x02 \x01(\x0b\x32\t.v2.Event\"K\n\x05\x45vent\x12\x1c\n\x06upload\x18\x01 \x01(\x0b\x32\x0c.v2.FileInfo\x12$\n\x0bhttpRequest\x18\x02 \x01(\x0b\x32\x0f.v2.HTTPRequest\"\xc3\x02\n\x08\x46ileInfo\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04size\x18\x02 \x01(\x03\x12\x16\n\x0esizeIsDeferred\x18\x03 \x01(\x08\x12\x0e\n\x06offset\x18\x04 \x01(\x03\x12,\n\x08metaData\x18\x05 \x03(\x0b\x32\x1a.v2.FileInfo.MetaDataEntry\x12\x11\n\tisPartial\x18\x06 \x01(\x08\x12\x0f\n\x07isFinal\x18\x07 \x01(\x08\x12\x16\n\x0epartialUploads\x18\x08 \x03(\t\x12*\n\x07storage\x18\t \x03(\x0b\x32\x19.v2.FileInfo.StorageEntry\x1a/\n\rMetaDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1a.\n\x0cStorageEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xe6\x01\n\x0f\x46ileInfoChanges\x12\n\n\x02id\x18\x01 \x01(\t\x12\x33\n\x08metaData\x18\x02 \x03(\x0b\x32!.v2.FileInfoChanges.MetaDataEntry\x12\x31\n\x07storage\x18\x03 \x03(\x0b\x32 .v2.FileInfoChanges.StorageEntry\x1a/\n\rMetaDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1a.\n\x0cStorageEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x9a\x01\n\x0bHTTPRequest\x12\x0e\n\x06method\x18\x01 \x01(\t\x12\x0b\n\x03uri\x18\x02 \x01(\t\x12\x12\n\nremoteAddr\x18\x03 \x01(\t\x12+\n\x06header\x18\x04 \x03(\x0b\x32\x1b.v2.HTTPRequest.HeaderEntry\x1a-\n\x0bHeaderEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x8d\x01\n\x0cHookResponse\x12&\n\x0chttpResponse\x18\x01 \x01(\x0b\x32\x10.v2.HTTPResponse\x12\x14\n\x0crejectUpload\x18\x02 \x01(\x08\x12+\n\x0e\x63hangeFileInfo\x18\x04 \x01(\x0b\x32\x13.v2.FileInfoChanges\x12\x12\n\nstopUpload\x18\x03 \x01(\x08\"\x90\x01\n\x0cHTTPResponse\x12\x12\n\nstatusCode\x18\x01 \x01(\x03\x12.\n\x07headers\x18\x02 \x03(\x0b\x32\x1d.v2.HTTPResponse.HeadersEntry\x12\x0c\n\x04\x62ody\x18\x03 \x01(\t\x1a.\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x32@\n\x0bHookHandler\x12\x31\n\nInvokeHook\x12\x0f.v2.HookRequest\x1a\x10.v2.HookResponse\"\x00\x62\x06proto3')
_HOOKREQUEST = DESCRIPTOR.message_types_by_name['HookRequest']
_EVENT = DESCRIPTOR.message_types_by_name['Event']
_FILEINFO = DESCRIPTOR.message_types_by_name['FileInfo']
_FILEINFO_METADATAENTRY = _FILEINFO.nested_types_by_name['MetaDataEntry']
_FILEINFO_STORAGEENTRY = _FILEINFO.nested_types_by_name['StorageEntry']
_HTTPREQUEST = DESCRIPTOR.message_types_by_name['HTTPRequest']
_HTTPREQUEST_HEADERENTRY = _HTTPREQUEST.nested_types_by_name['HeaderEntry']
_HOOKRESPONSE = DESCRIPTOR.message_types_by_name['HookResponse']
_HTTPRESPONSE = DESCRIPTOR.message_types_by_name['HTTPResponse']
_HTTPRESPONSE_HEADERSENTRY = _HTTPRESPONSE.nested_types_by_name['HeadersEntry']
HookRequest = _reflection.GeneratedProtocolMessageType('HookRequest', (_message.Message,), {
'DESCRIPTOR' : _HOOKREQUEST,
'__module__' : 'hook_pb2'
# @@protoc_insertion_point(class_scope:v2.HookRequest)
})
_sym_db.RegisterMessage(HookRequest)
Event = _reflection.GeneratedProtocolMessageType('Event', (_message.Message,), {
'DESCRIPTOR' : _EVENT,
'__module__' : 'hook_pb2'
# @@protoc_insertion_point(class_scope:v2.Event)
})
_sym_db.RegisterMessage(Event)
FileInfo = _reflection.GeneratedProtocolMessageType('FileInfo', (_message.Message,), {
'MetaDataEntry' : _reflection.GeneratedProtocolMessageType('MetaDataEntry', (_message.Message,), {
'DESCRIPTOR' : _FILEINFO_METADATAENTRY,
'__module__' : 'hook_pb2'
# @@protoc_insertion_point(class_scope:v2.FileInfo.MetaDataEntry)
})
,
'StorageEntry' : _reflection.GeneratedProtocolMessageType('StorageEntry', (_message.Message,), {
'DESCRIPTOR' : _FILEINFO_STORAGEENTRY,
'__module__' : 'hook_pb2'
# @@protoc_insertion_point(class_scope:v2.FileInfo.StorageEntry)
})
,
'DESCRIPTOR' : _FILEINFO,
'__module__' : 'hook_pb2'
# @@protoc_insertion_point(class_scope:v2.FileInfo)
})
_sym_db.RegisterMessage(FileInfo)
_sym_db.RegisterMessage(FileInfo.MetaDataEntry)
_sym_db.RegisterMessage(FileInfo.StorageEntry)
HTTPRequest = _reflection.GeneratedProtocolMessageType('HTTPRequest', (_message.Message,), {
'HeaderEntry' : _reflection.GeneratedProtocolMessageType('HeaderEntry', (_message.Message,), {
'DESCRIPTOR' : _HTTPREQUEST_HEADERENTRY,
'__module__' : 'hook_pb2'
# @@protoc_insertion_point(class_scope:v2.HTTPRequest.HeaderEntry)
})
,
'DESCRIPTOR' : _HTTPREQUEST,
'__module__' : 'hook_pb2'
# @@protoc_insertion_point(class_scope:v2.HTTPRequest)
})
_sym_db.RegisterMessage(HTTPRequest)
_sym_db.RegisterMessage(HTTPRequest.HeaderEntry)
HookResponse = _reflection.GeneratedProtocolMessageType('HookResponse', (_message.Message,), {
'DESCRIPTOR' : _HOOKRESPONSE,
'__module__' : 'hook_pb2'
# @@protoc_insertion_point(class_scope:v2.HookResponse)
})
_sym_db.RegisterMessage(HookResponse)
HTTPResponse = _reflection.GeneratedProtocolMessageType('HTTPResponse', (_message.Message,), {
'HeadersEntry' : _reflection.GeneratedProtocolMessageType('HeadersEntry', (_message.Message,), {
'DESCRIPTOR' : _HTTPRESPONSE_HEADERSENTRY,
'__module__' : 'hook_pb2'
# @@protoc_insertion_point(class_scope:v2.HTTPResponse.HeadersEntry)
})
,
'DESCRIPTOR' : _HTTPRESPONSE,
'__module__' : 'hook_pb2'
# @@protoc_insertion_point(class_scope:v2.HTTPResponse)
})
_sym_db.RegisterMessage(HTTPResponse)
_sym_db.RegisterMessage(HTTPResponse.HeadersEntry)
_HOOKHANDLER = DESCRIPTOR.services_by_name['HookHandler']
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'hook_pb2', globals())
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
@ -110,6 +24,10 @@ if _descriptor._USE_C_DESCRIPTORS == False:
_FILEINFO_METADATAENTRY._serialized_options = b'8\001'
_FILEINFO_STORAGEENTRY._options = None
_FILEINFO_STORAGEENTRY._serialized_options = b'8\001'
_FILEINFOCHANGES_METADATAENTRY._options = None
_FILEINFOCHANGES_METADATAENTRY._serialized_options = b'8\001'
_FILEINFOCHANGES_STORAGEENTRY._options = None
_FILEINFOCHANGES_STORAGEENTRY._serialized_options = b'8\001'
_HTTPREQUEST_HEADERENTRY._options = None
_HTTPREQUEST_HEADERENTRY._serialized_options = b'8\001'
_HTTPRESPONSE_HEADERSENTRY._options = None
@ -124,16 +42,22 @@ if _descriptor._USE_C_DESCRIPTORS == False:
_FILEINFO_METADATAENTRY._serialized_end=426
_FILEINFO_STORAGEENTRY._serialized_start=428
_FILEINFO_STORAGEENTRY._serialized_end=474
_HTTPREQUEST._serialized_start=477
_HTTPREQUEST._serialized_end=631
_HTTPREQUEST_HEADERENTRY._serialized_start=586
_HTTPREQUEST_HEADERENTRY._serialized_end=631
_HOOKRESPONSE._serialized_start=633
_HOOKRESPONSE._serialized_end=729
_HTTPRESPONSE._serialized_start=732
_HTTPRESPONSE._serialized_end=876
_HTTPRESPONSE_HEADERSENTRY._serialized_start=830
_HTTPRESPONSE_HEADERSENTRY._serialized_end=876
_HOOKHANDLER._serialized_start=878
_HOOKHANDLER._serialized_end=942
_FILEINFOCHANGES._serialized_start=477
_FILEINFOCHANGES._serialized_end=707
_FILEINFOCHANGES_METADATAENTRY._serialized_start=379
_FILEINFOCHANGES_METADATAENTRY._serialized_end=426
_FILEINFOCHANGES_STORAGEENTRY._serialized_start=428
_FILEINFOCHANGES_STORAGEENTRY._serialized_end=474
_HTTPREQUEST._serialized_start=710
_HTTPREQUEST._serialized_end=864
_HTTPREQUEST_HEADERENTRY._serialized_start=819
_HTTPREQUEST_HEADERENTRY._serialized_end=864
_HOOKRESPONSE._serialized_start=867
_HOOKRESPONSE._serialized_end=1008
_HTTPRESPONSE._serialized_start=1011
_HTTPRESPONSE._serialized_end=1155
_HTTPRESPONSE_HEADERSENTRY._serialized_start=1109
_HTTPRESPONSE_HEADERSENTRY._serialized_end=1155
_HOOKHANDLER._serialized_start=1157
_HOOKHANDLER._serialized_end=1221
# @@protoc_insertion_point(module_scope)

View File

@ -27,7 +27,11 @@ class HookHandlerServicer(object):
"""
def InvokeHook(self, request, context):
"""Sends a hook
"""InvokeHook is invoked for every hook that is executed. HookRequest contains the
corresponding information about the hook type, the involved upload, and
causing HTTP request.
The return value HookResponse allows to stop or reject an upload, as well as modifying
the HTTP response. See the documentation for HookResponse for more details.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')

View File

@ -1,6 +1,7 @@
import grpc
from concurrent import futures
import time
import uuid
import hook_pb2_grpc as pb2_grpc
import hook_pb2 as pb2
@ -19,13 +20,22 @@ class HookHandler(pb2_grpc.HookHandlerServicer):
# Example: Use the pre-create hook to check if a filename has been supplied
# using metadata. If not, the upload is rejected with a custom HTTP response.
# In addition, a custom upload ID with a choosable prefix is supplied.
# Metadata is configured, so that it only retains the filename meta data
# and the creation time.
if hook_request.type == 'pre-create':
filename = hook_request.event.upload.metaData['filename']
if filename == "":
metaData = hook_request.event.upload.metaData
isValid = 'filename' in metaData
if not isValid:
hook_response.rejectUpload = True
hook_response.httpResponse.statusCode = 400
hook_response.httpResponse.body = 'no filename provided'
hook_response.httpResponse.headers['X-Some-Header'] = 'yes'
else:
hook_response.changeFileInfo.id = f'prefix-{uuid.uuid4()}'
hook_response.changeFileInfo.metaData
hook_response.changeFileInfo.metaData['filename'] = metaData['filename']
hook_response.changeFileInfo.metaData['creation_time'] = time.ctime()
# Example: Use the post-finish hook to print information about a completed upload,
# including its storage location.

View File

@ -2,6 +2,8 @@ from http.server import HTTPServer, BaseHTTPRequestHandler
from io import BytesIO
import json
import time
import uuid
class HTTPHookHandler(BaseHTTPRequestHandler):
@ -29,13 +31,25 @@ class HTTPHookHandler(BaseHTTPRequestHandler):
# Example: Use the pre-create hook to check if a filename has been supplied
# using metadata. If not, the upload is rejected with a custom HTTP response.
# In addition, a custom upload ID with a choosable prefix is supplied.
# Metadata is configured, so that it only retains the filename meta data
# and the creation time.
if hook_request['Type'] == 'pre-create':
metaData = hook_request['Event']['Upload']['MetaData']
if 'filename' not in metaData:
isValid = 'filename' in metaData
if not isValid:
hook_response['RejectUpload'] = True
hook_response['HTTPResponse']['StatusCode'] = 400
hook_response['HTTPResponse']['Body'] = 'no filename provided'
hook_response['HTTPResponse']['Headers']['X-Some-Header'] = 'yes'
else:
hook_response['ChangeFileInfo'] = {}
hook_response['ChangeFileInfo']['ID'] = f'prefix-{uuid.uuid4()}'
hook_response['ChangeFileInfo']['MetaData'] = {
'filename': metaData['filename'],
'creation_time': time.ctime(),
}
# Example: Use the post-finish hook to print information about a completed upload,
# including its storage location.

2
go.mod
View File

@ -13,7 +13,6 @@ require (
github.com/felixge/fgprof v0.9.2
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d
github.com/golang/mock v1.6.0
github.com/golang/protobuf v1.5.3
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0
github.com/hashicorp/go-hclog v0.14.1
github.com/hashicorp/go-plugin v1.4.3
@ -24,6 +23,7 @@ require (
github.com/vimeo/go-util v1.4.1
google.golang.org/api v0.125.0
google.golang.org/grpc v1.55.0
google.golang.org/protobuf v1.30.0
gopkg.in/Acconut/lockfile.v1 v1.1.0
gopkg.in/h2non/gock.v1 v1.1.2
)

View File

@ -60,7 +60,10 @@ type Config struct {
// If the error is non-nil, the upload will not be created. This can be used to implement
// validation of upload metadata etc. Furthermore, HTTPResponse will be ignored and
// the error value can contain values for the HTTP response.
PreUploadCreateCallback func(hook HookEvent) (HTTPResponse, error)
// If the error is nil, FileInfoChanges can be filled out to specify individual properties
// that should be overwriten before the upload is create. See its type definition for
// more details on its behavior. If you do not want to make any changes, return an empty struct.
PreUploadCreateCallback func(hook HookEvent) (HTTPResponse, FileInfoChanges, error)
// PreFinishResponseCallback will be invoked after an upload is completed but before
// a response is returned to the client. This can be used to implement post-processing validation.
// If the callback returns no error, optional values from HTTPResponse will be contained in the HTTP response.

View File

@ -50,6 +50,34 @@ func (f FileInfo) StopUpload() {
}
}
// FileInfoChanges collects changes the should be made to a FileInfo struct. This
// can be done using the PreUploadCreateCallback to modify certain properties before
// an upload is created. Properties which should not be modified (e.g. Size or Offset)
// are intentionally left out here.
type FileInfoChanges struct {
// If ID is not empty, it will be passed to the data store, allowing
// hooks to influence the upload ID. Be aware that a data store is not required to
// respect a pre-defined upload ID and might overwrite or modify it. However,
// all data stores in the github.com/tus/tusd package do respect pre-defined IDs.
ID string
// If MetaData is not nil, it replaces the entire user-defined meta data from
// the upload creation request. You can add custom meta data fields this way
// or ensure that only certain fields from the user-defined meta data are saved.
// If you want to retain only specific entries from the user-defined meta data, you must
// manually copy them into this MetaData field.
// If you do not want to store any meta data, set this field to an empty map (`MetaData{}`).
// If you want to keep the entire user-defined meta data, set this field to nil.
MetaData MetaData
// If Storage is not nil, it is passed to the data store to allow for minor adjustments
// to the upload storage (e.g. destination file name). The details are specific for each
// data store and should be looked up in their respective documentation.
// Please be aware that this behavior is currently not supported by any data store in
// the github.com/tus/tusd package.
Storage map[string]string
}
type Upload interface {
// Write the chunk read from src into the file specified by the id at the
// given offset. The handler will take care of validating the offset and

View File

@ -322,12 +322,25 @@ func (handler *UnroutedHandler) PostFile(w http.ResponseWriter, r *http.Request)
}
if handler.config.PreUploadCreateCallback != nil {
resp2, err := handler.config.PreUploadCreateCallback(newHookEvent(info, r))
resp2, changes, err := handler.config.PreUploadCreateCallback(newHookEvent(info, r))
if err != nil {
handler.sendError(c, err)
return
}
resp = resp.MergeWith(resp2)
// Apply changes returned from the pre-create hook.
if changes.ID != "" {
info.ID = changes.ID
}
if changes.MetaData != nil {
info.MetaData = changes.MetaData
}
if changes.Storage != nil {
info.Storage = changes.Storage
}
}
upload, err := handler.composer.Core.NewUpload(c, info)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,115 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.2.0
// - protoc v3.21.12
// source: cmd/tusd/cli/hooks/proto/v2/hook.proto
package v2
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.32.0 or later.
const _ = grpc.SupportPackageIsVersion7
// HookHandlerClient is the client API for HookHandler service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type HookHandlerClient interface {
// InvokeHook is invoked for every hook that is executed. HookRequest contains the
// corresponding information about the hook type, the involved upload, and
// causing HTTP request.
// The return value HookResponse allows to stop or reject an upload, as well as modifying
// the HTTP response. See the documentation for HookResponse for more details.
InvokeHook(ctx context.Context, in *HookRequest, opts ...grpc.CallOption) (*HookResponse, error)
}
type hookHandlerClient struct {
cc grpc.ClientConnInterface
}
func NewHookHandlerClient(cc grpc.ClientConnInterface) HookHandlerClient {
return &hookHandlerClient{cc}
}
func (c *hookHandlerClient) InvokeHook(ctx context.Context, in *HookRequest, opts ...grpc.CallOption) (*HookResponse, error) {
out := new(HookResponse)
err := c.cc.Invoke(ctx, "/v2.HookHandler/InvokeHook", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// HookHandlerServer is the server API for HookHandler service.
// All implementations must embed UnimplementedHookHandlerServer
// for forward compatibility
type HookHandlerServer interface {
// InvokeHook is invoked for every hook that is executed. HookRequest contains the
// corresponding information about the hook type, the involved upload, and
// causing HTTP request.
// The return value HookResponse allows to stop or reject an upload, as well as modifying
// the HTTP response. See the documentation for HookResponse for more details.
InvokeHook(context.Context, *HookRequest) (*HookResponse, error)
mustEmbedUnimplementedHookHandlerServer()
}
// UnimplementedHookHandlerServer must be embedded to have forward compatible implementations.
type UnimplementedHookHandlerServer struct {
}
func (UnimplementedHookHandlerServer) InvokeHook(context.Context, *HookRequest) (*HookResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method InvokeHook not implemented")
}
func (UnimplementedHookHandlerServer) mustEmbedUnimplementedHookHandlerServer() {}
// UnsafeHookHandlerServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to HookHandlerServer will
// result in compilation errors.
type UnsafeHookHandlerServer interface {
mustEmbedUnimplementedHookHandlerServer()
}
func RegisterHookHandlerServer(s grpc.ServiceRegistrar, srv HookHandlerServer) {
s.RegisterService(&HookHandler_ServiceDesc, srv)
}
func _HookHandler_InvokeHook_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(HookRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(HookHandlerServer).InvokeHook(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/v2.HookHandler/InvokeHook",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(HookHandlerServer).InvokeHook(ctx, req.(*HookRequest))
}
return interceptor(ctx, in, info, handler)
}
// HookHandler_ServiceDesc is the grpc.ServiceDesc for HookHandler service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var HookHandler_ServiceDesc = grpc.ServiceDesc{
ServiceName: "v2.HookHandler",
HandlerType: (*HookHandlerServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "InvokeHook",
Handler: _HookHandler_InvokeHook_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "cmd/tusd/cli/hooks/proto/v2/hook.proto",
}