v2: Rework hooks system (#516)

* ci: Remove plugin hook handler

* Rework error type from interface to struct

* Avoid writing to http.ResponseWriter directly

* Allow hooks to modify response

* Add example for HTTP hooks using Python

* Implement new plugin system using Hashicorp/go-plugin

* Enable returning partial HTTPResponses

* Remove some (unnecessary) error handling

* Forward stdout and stderr from plugin to tusd

* docs: Update examples

* cli: Update filehooks to new system

* cli: Renovate gRPC hooks

* docs: Correct casing of gRPC

* misc: Documentation, better examples, and code structure
This commit is contained in:
Marius 2022-03-02 00:36:49 +01:00 committed by GitHub
parent a05c090d05
commit 12c10bf62f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 1808 additions and 970 deletions

2
.gitignore vendored
View File

@ -5,3 +5,5 @@ node_modules/
.DS_Store .DS_Store
./tusd ./tusd
tusd_*_* tusd_*_*
__pycache__/
examples/hooks/plugin/hook_handler

View File

@ -38,6 +38,7 @@ var Flags struct {
AzObjectPrefix string AzObjectPrefix string
AzEndpoint string AzEndpoint string
EnabledHooksString string EnabledHooksString string
PluginHookPath string
FileHooksDir string FileHooksDir string
HttpHooksEndpoint string HttpHooksEndpoint string
HttpHooksForwardHeaders string HttpHooksForwardHeaders string
@ -46,8 +47,6 @@ var Flags struct {
GrpcHooksEndpoint string GrpcHooksEndpoint string
GrpcHooksRetry int GrpcHooksRetry int
GrpcHooksBackoff int GrpcHooksBackoff int
HooksStopUploadCode int
PluginHookPath string
EnabledHooks []hooks.HookType EnabledHooks []hooks.HookType
ShowVersion bool ShowVersion bool
ExposeMetrics bool ExposeMetrics bool
@ -91,6 +90,7 @@ func ParseFlags() {
flag.StringVar(&Flags.AzObjectPrefix, "azure-object-prefix", "", "Prefix for Azure object names") flag.StringVar(&Flags.AzObjectPrefix, "azure-object-prefix", "", "Prefix for Azure object names")
flag.StringVar(&Flags.AzEndpoint, "azure-endpoint", "", "Custom Endpoint to use for Azure BlockBlob Storage (requires azure-storage to be pass)") flag.StringVar(&Flags.AzEndpoint, "azure-endpoint", "", "Custom Endpoint to use for Azure BlockBlob Storage (requires azure-storage to be pass)")
flag.StringVar(&Flags.EnabledHooksString, "hooks-enabled-events", "pre-create,post-create,post-receive,post-terminate,post-finish", "Comma separated list of enabled hook events (e.g. post-create,post-finish). Leave empty to enable default events") flag.StringVar(&Flags.EnabledHooksString, "hooks-enabled-events", "pre-create,post-create,post-receive,post-terminate,post-finish", "Comma separated list of enabled hook events (e.g. post-create,post-finish). Leave empty to enable default events")
flag.StringVar(&Flags.PluginHookPath, "hooks-plugin", "", "Path to a Go plugin for loading hook functions")
flag.StringVar(&Flags.FileHooksDir, "hooks-dir", "", "Directory to search for available hooks scripts") flag.StringVar(&Flags.FileHooksDir, "hooks-dir", "", "Directory to search for available hooks scripts")
flag.StringVar(&Flags.HttpHooksEndpoint, "hooks-http", "", "An HTTP endpoint to which hook events will be sent to") flag.StringVar(&Flags.HttpHooksEndpoint, "hooks-http", "", "An HTTP endpoint to which hook events will be sent to")
flag.StringVar(&Flags.HttpHooksForwardHeaders, "hooks-http-forward-headers", "", "List of HTTP request headers to be forwarded from the client request to the hook endpoint") flag.StringVar(&Flags.HttpHooksForwardHeaders, "hooks-http-forward-headers", "", "List of HTTP request headers to be forwarded from the client request to the hook endpoint")
@ -99,8 +99,6 @@ func ParseFlags() {
flag.StringVar(&Flags.GrpcHooksEndpoint, "hooks-grpc", "", "An gRPC endpoint to which hook events will be sent to") flag.StringVar(&Flags.GrpcHooksEndpoint, "hooks-grpc", "", "An gRPC endpoint to which hook events will be sent to")
flag.IntVar(&Flags.GrpcHooksRetry, "hooks-grpc-retry", 3, "Number of times to retry on a server error or network timeout") flag.IntVar(&Flags.GrpcHooksRetry, "hooks-grpc-retry", 3, "Number of times to retry on a server error or network timeout")
flag.IntVar(&Flags.GrpcHooksBackoff, "hooks-grpc-backoff", 1, "Number of seconds to wait before retrying each retry") flag.IntVar(&Flags.GrpcHooksBackoff, "hooks-grpc-backoff", 1, "Number of seconds to wait before retrying each retry")
flag.IntVar(&Flags.HooksStopUploadCode, "hooks-stop-code", 0, "Return code from post-receive hook which causes tusd to stop and delete the current upload. A zero value means that no uploads will be stopped")
flag.StringVar(&Flags.PluginHookPath, "hooks-plugin", "", "Path to a Go plugin for loading hook functions (only supported on Linux and macOS; highly EXPERIMENTAL and may BREAK in the future)")
flag.BoolVar(&Flags.ShowVersion, "version", false, "Print tusd version information") flag.BoolVar(&Flags.ShowVersion, "version", false, "Print tusd version information")
flag.BoolVar(&Flags.ExposeMetrics, "expose-metrics", true, "Expose metrics about tusd usage") flag.BoolVar(&Flags.ExposeMetrics, "expose-metrics", true, "Expose metrics about tusd usage")
flag.StringVar(&Flags.MetricsPath, "metrics-path", "/metrics", "Path under which the metrics endpoint will be accessible") flag.StringVar(&Flags.MetricsPath, "metrics-path", "/metrics", "Path under which the metrics endpoint will be accessible")

View File

@ -1,7 +1,6 @@
package cli package cli
import ( import (
"fmt"
"strconv" "strconv"
"strings" "strings"
@ -20,27 +19,12 @@ func hookTypeInSlice(a hooks.HookType, list []hooks.HookType) bool {
return false return false
} }
func hookCallback(typ hooks.HookType, info handler.HookEvent) error { func preCreateCallback(event handler.HookEvent) (handler.HTTPResponse, error) {
if output, err := invokeHookSync(typ, info, true); err != nil { return invokeHookSync(hooks.HookPreCreate, event)
if hookErr, ok := err.(hooks.HookError); ok {
return hooks.NewHookError(
fmt.Errorf("%s hook failed: %s", typ, err),
hookErr.StatusCode(),
hookErr.Body(),
)
}
return fmt.Errorf("%s hook failed: %s\n%s", typ, err, string(output))
} }
return nil func preFinishCallback(event handler.HookEvent) (handler.HTTPResponse, error) {
} return invokeHookSync(hooks.HookPreFinish, event)
func preCreateCallback(info handler.HookEvent) error {
return hookCallback(hooks.HookPreCreate, info)
}
func preFinishCallback(info handler.HookEvent) error {
return hookCallback(hooks.HookPreFinish, info)
} }
func SetupHookMetrics() { func SetupHookMetrics() {
@ -113,35 +97,35 @@ func SetupPostHooks(handler *handler.Handler) {
go func() { go func() {
for { for {
select { select {
case info := <-handler.CompleteUploads: case event := <-handler.CompleteUploads:
invokeHookAsync(hooks.HookPostFinish, info) invokeHookAsync(hooks.HookPostFinish, event)
case info := <-handler.TerminatedUploads: case event := <-handler.TerminatedUploads:
invokeHookAsync(hooks.HookPostTerminate, info) invokeHookAsync(hooks.HookPostTerminate, event)
case info := <-handler.UploadProgress: case event := <-handler.UploadProgress:
invokeHookAsync(hooks.HookPostReceive, info) invokeHookAsync(hooks.HookPostReceive, event)
case info := <-handler.CreatedUploads: case event := <-handler.CreatedUploads:
invokeHookAsync(hooks.HookPostCreate, info) invokeHookAsync(hooks.HookPostCreate, event)
} }
} }
}() }()
} }
func invokeHookAsync(typ hooks.HookType, info handler.HookEvent) { func invokeHookAsync(typ hooks.HookType, event handler.HookEvent) {
go func() { go func() {
// Error handling is taken care by the function. // Error handling is taken care by the function.
_, _ = invokeHookSync(typ, info, false) _, _ = invokeHookSync(typ, event)
}() }()
} }
func invokeHookSync(typ hooks.HookType, info handler.HookEvent, captureOutput bool) ([]byte, error) { func invokeHookSync(typ hooks.HookType, event handler.HookEvent) (httpRes handler.HTTPResponse, err error) {
if !hookTypeInSlice(typ, Flags.EnabledHooks) { if !hookTypeInSlice(typ, Flags.EnabledHooks) {
return nil, nil return httpRes, nil
} }
MetricsHookInvocationsTotal.WithLabelValues(string(typ)).Add(1) MetricsHookInvocationsTotal.WithLabelValues(string(typ)).Add(1)
id := info.Upload.ID id := event.Upload.ID
size := info.Upload.Size size := event.Upload.Size
switch typ { switch typ {
case hooks.HookPostFinish: case hooks.HookPostFinish:
@ -151,28 +135,43 @@ func invokeHookSync(typ hooks.HookType, info handler.HookEvent, captureOutput bo
} }
if hookHandler == nil { if hookHandler == nil {
return nil, nil return httpRes, nil
} }
name := string(typ)
if Flags.VerboseOutput { if Flags.VerboseOutput {
logEv(stdout, "HookInvocationStart", "type", name, "id", id) logEv(stdout, "HookInvocationStart", "type", string(typ), "id", id)
} }
output, returnCode, err := hookHandler.InvokeHook(typ, info, captureOutput) hookRes, err := hookHandler.InvokeHook(hooks.HookRequest{
Type: typ,
Event: event,
})
if err != nil { if err != nil {
logEv(stderr, "HookInvocationError", "type", string(typ), "id", id, "error", err.Error()) logEv(stderr, "HookInvocationError", "type", string(typ), "id", id, "error", err.Error())
MetricsHookErrorsTotal.WithLabelValues(string(typ)).Add(1) MetricsHookErrorsTotal.WithLabelValues(string(typ)).Add(1)
return httpRes, err
} else if Flags.VerboseOutput { } else if Flags.VerboseOutput {
logEv(stdout, "HookInvocationFinish", "type", string(typ), "id", id) logEv(stdout, "HookInvocationFinish", "type", string(typ), "id", id)
} }
if typ == hooks.HookPostReceive && Flags.HooksStopUploadCode != 0 && Flags.HooksStopUploadCode == returnCode { httpRes = hookRes.HTTPResponse
// If the hook response includes the instruction to reject the upload, reuse the error code
// and message from ErrUploadRejectedByServer, but also include custom HTTP response values
if typ == hooks.HookPreCreate && hookRes.RejectUpload {
err := handler.ErrUploadRejectedByServer
err.HTTPResponse = err.HTTPResponse.MergeWith(httpRes)
return httpRes, err
}
if typ == hooks.HookPostReceive && hookRes.StopUpload {
logEv(stdout, "HookStopUpload", "id", id) logEv(stdout, "HookStopUpload", "id", id)
info.Upload.StopUpload() // TODO: Control response for PATCH request
event.Upload.StopUpload()
} }
return output, err return httpRes, err
} }

View File

@ -3,11 +3,10 @@ package hooks
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt"
"os" "os"
"os/exec" "os/exec"
"strconv" "strconv"
"github.com/tus/tusd/pkg/handler"
) )
type FileHook struct { type FileHook struct {
@ -18,43 +17,50 @@ func (_ FileHook) Setup() error {
return nil return nil
} }
func (h FileHook) InvokeHook(typ HookType, info handler.HookEvent, captureOutput bool) ([]byte, int, error) { func (h FileHook) InvokeHook(req HookRequest) (res HookResponse, err error) {
hookPath := h.Directory + string(os.PathSeparator) + string(typ) hookPath := h.Directory + string(os.PathSeparator) + string(req.Type)
cmd := exec.Command(hookPath) cmd := exec.Command(hookPath)
env := os.Environ() env := os.Environ()
env = append(env, "TUS_ID="+info.Upload.ID) env = append(env, "TUS_ID="+req.Event.Upload.ID)
env = append(env, "TUS_SIZE="+strconv.FormatInt(info.Upload.Size, 10)) env = append(env, "TUS_SIZE="+strconv.FormatInt(req.Event.Upload.Size, 10))
env = append(env, "TUS_OFFSET="+strconv.FormatInt(info.Upload.Offset, 10)) env = append(env, "TUS_OFFSET="+strconv.FormatInt(req.Event.Upload.Offset, 10))
jsonInfo, err := json.Marshal(info) jsonReq, err := json.Marshal(req)
if err != nil { if err != nil {
return nil, 0, err return res, err
} }
reader := bytes.NewReader(jsonInfo) reader := bytes.NewReader(jsonReq)
cmd.Stdin = reader cmd.Stdin = reader
cmd.Env = env cmd.Env = env
cmd.Dir = h.Directory cmd.Dir = h.Directory
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
// If `captureOutput` is true, this function will return the output (both, output, err := cmd.Output()
// stderr and stdout), else it will use this process' stdout
var output []byte
if !captureOutput {
cmd.Stdout = os.Stdout
err = cmd.Run()
} else {
output, err = cmd.Output()
}
// Ignore the error, only, if the hook's file could not be found. This usually // Ignore the error if the hook's file could not be found. This usually
// means that the user is only using a subset of the available hooks. // means that the user is only using a subset of the available hooks.
if os.IsNotExist(err) { if os.IsNotExist(err) {
err = nil return res, nil
} }
returnCode := cmd.ProcessState.ExitCode() // Report error if the exit code was non-zero
if err, ok := err.(*exec.ExitError); ok {
return output, returnCode, err return res, fmt.Errorf("unexpected return code %d from hook endpoint: %s", err.ProcessState.ExitCode(), string(output))
}
if err != nil {
return res, err
}
// Do not parse the output as JSON, if we received no output to reduce possible
// errors.
if len(output) > 0 {
if err = json.Unmarshal(output, &res); err != nil {
return res, fmt.Errorf("failed to parse hook response: %w, response was: %s", err, string(output))
}
}
return res, nil
} }

View File

@ -5,17 +5,15 @@ import (
"time" "time"
grpc_retry "github.com/grpc-ecosystem/go-grpc-middleware/retry" grpc_retry "github.com/grpc-ecosystem/go-grpc-middleware/retry"
"github.com/tus/tusd/pkg/handler" pb "github.com/tus/tusd/pkg/proto/v2"
pb "github.com/tus/tusd/pkg/proto/v1"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/status"
) )
type GrpcHook struct { type GrpcHook struct {
Endpoint string Endpoint string
MaxRetries int MaxRetries int
Backoff int Backoff int
Client pb.HookServiceClient Client pb.HookHandlerClient
} }
func (g *GrpcHook) Setup() error { func (g *GrpcHook) Setup() error {
@ -31,44 +29,59 @@ func (g *GrpcHook) Setup() error {
if err != nil { if err != nil {
return err return err
} }
g.Client = pb.NewHookServiceClient(conn) g.Client = pb.NewHookHandlerClient(conn)
return nil return nil
} }
func (g *GrpcHook) InvokeHook(typ HookType, info handler.HookEvent, captureOutput bool) ([]byte, int, error) { func (g *GrpcHook) InvokeHook(hookReq HookRequest) (hookRes HookResponse, err error) {
ctx := context.Background() ctx := context.Background()
req := &pb.SendRequest{Hook: marshal(typ, info)} req := marshal(hookReq)
resp, err := g.Client.Send(ctx, req) res, err := g.Client.InvokeHook(ctx, req)
if err != nil { if err != nil {
if e, ok := status.FromError(err); ok { return hookRes, err
return nil, int(e.Code()), err
}
return nil, 2, err
}
if captureOutput {
return resp.Response.GetValue(), 0, err
}
return nil, 0, err
} }
func marshal(typ HookType, info handler.HookEvent) *pb.Hook { hookRes = unmarshal(res)
return &pb.Hook{ return hookRes, nil
Upload: &pb.Upload{ }
Id: info.Upload.ID,
Size: info.Upload.Size, func marshal(hookReq HookRequest) *pb.HookRequest {
SizeIsDeferred: info.Upload.SizeIsDeferred, event := hookReq.Event
Offset: info.Upload.Offset,
MetaData: info.Upload.MetaData, return &pb.HookRequest{
IsPartial: info.Upload.IsPartial, Type: string(hookReq.Type),
IsFinal: info.Upload.IsFinal, Event: &pb.Event{
PartialUploads: info.Upload.PartialUploads, Upload: &pb.FileInfo{
Storage: info.Upload.Storage, Id: event.Upload.ID,
Size: event.Upload.Size,
SizeIsDeferred: event.Upload.SizeIsDeferred,
Offset: event.Upload.Offset,
MetaData: event.Upload.MetaData,
IsPartial: event.Upload.IsPartial,
IsFinal: event.Upload.IsFinal,
PartialUploads: event.Upload.PartialUploads,
Storage: event.Upload.Storage,
}, },
HttpRequest: &pb.HTTPRequest{ HttpRequest: &pb.HTTPRequest{
Method: info.HTTPRequest.Method, Method: event.HTTPRequest.Method,
Uri: info.HTTPRequest.URI, Uri: event.HTTPRequest.URI,
RemoteAddr: info.HTTPRequest.RemoteAddr, RemoteAddr: event.HTTPRequest.RemoteAddr,
// TODO: HEaders
},
}, },
Name: string(typ),
} }
} }
func unmarshal(res *pb.HookResponse) (hookRes HookResponse) {
hookRes.RejectUpload = res.RejectUpload
hookRes.StopUpload = res.StopUpload
httpRes := res.HttpResponse
if httpRes != nil {
hookRes.HTTPResponse.StatusCode = int(httpRes.StatusCode)
hookRes.HTTPResponse.Headers = httpRes.Headers
hookRes.HTTPResponse.Body = httpRes.Body
}
return hookRes
}

View File

@ -1,12 +1,58 @@
package hooks package hooks
// TODO: Move hooks into a package in /pkg
import ( import (
"github.com/tus/tusd/pkg/handler" "github.com/tus/tusd/pkg/handler"
) )
// HookHandler is the main inferface to be implemented by all hook backends.
type HookHandler interface { type HookHandler interface {
// Setup is invoked once the hook backend is initalized.
Setup() error Setup() error
InvokeHook(typ HookType, info handler.HookEvent, captureOutput bool) ([]byte, int, error) // InvokeHook is invoked for every hook that is executed. req contains the
// corresponding information about the hook type, the involved upload, and
// causing HTTP request.
// The return value res allows to stop or reject an upload, as well as modifying
// the HTTP response. See the documentation for HookResponse for more details.
// If err is not nil, the value of res will be ignored. err should only be
// non-nil if the hook failed to complete successfully.
InvokeHook(req HookRequest) (res HookResponse, err error)
}
// HookRequest contains the information about the hook type, the involved upload,
// and causing HTTP request.
type HookRequest struct {
// Type is the name of the hook.
Type HookType
// Event contains the involved upload and causing HTTP request.
Event handler.HookEvent
}
// HookResponse is the response after a hook is executed.
type HookResponse struct {
// HTTPResponse's fields can be filled to modify the HTTP response.
// This is only possible for pre-create, pre-finish and post-receive hooks.
// For other hooks this value is ignored.
// If multiple hooks modify the HTTP response, a later hook may overwrite the
// modified values from a previous hook (e.g. if multiple post-receive hooks
// are executed).
// Example usages: Send an error to the client if RejectUpload/StopUpload are
// set in the pre-create/post-receive hook. Send more information to the client
// in the pre-finish hook.
HTTPResponse handler.HTTPResponse
// RejectUpload will cause the upload to be rejected and not be created during
// POST request. This value is only respected for pre-create hooks. For other hooks,
// it is ignored. Use the HTTPResponse field to send details about the rejection
// to the client.
RejectUpload bool
// 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
// to the client.
StopUpload bool
} }
type HookType string type HookType string
@ -21,29 +67,3 @@ const (
) )
var AvailableHooks []HookType = []HookType{HookPreCreate, HookPostCreate, HookPostReceive, HookPostTerminate, HookPostFinish, HookPreFinish} var AvailableHooks []HookType = []HookType{HookPreCreate, HookPostCreate, HookPostReceive, HookPostTerminate, HookPostFinish, HookPreFinish}
type hookDataStore struct {
handler.DataStore
}
type HookError struct {
error
statusCode int
body []byte
}
func NewHookError(err error, statusCode int, body []byte) HookError {
return HookError{err, statusCode, body}
}
func (herr HookError) StatusCode() int {
return herr.statusCode
}
func (herr HookError) Body() []byte {
return herr.body
}
func (herr HookError) Error() string {
return herr.error.Error()
}

View File

@ -8,8 +8,6 @@ import (
"net/http" "net/http"
"time" "time"
"github.com/tus/tusd/pkg/handler"
"github.com/sethgrid/pester" "github.com/sethgrid/pester"
) )
@ -18,35 +16,11 @@ type HttpHook struct {
MaxRetries int MaxRetries int
Backoff int Backoff int
ForwardHeaders []string ForwardHeaders []string
client *pester.Client
} }
func (_ HttpHook) Setup() error { func (h *HttpHook) Setup() error {
return nil
}
func (h HttpHook) InvokeHook(typ HookType, info handler.HookEvent, captureOutput bool) ([]byte, int, error) {
jsonInfo, err := json.Marshal(info)
if err != nil {
return nil, 0, err
}
req, err := http.NewRequest("POST", h.Endpoint, bytes.NewBuffer(jsonInfo))
if err != nil {
return nil, 0, err
}
for _, k := range h.ForwardHeaders {
// Lookup the Canonicalised version of the specified header
if vals, ok := info.HTTPRequest.Header[http.CanonicalHeaderKey(k)]; ok {
// but set the case specified by the user
req.Header[k] = vals
}
}
req.Header.Set("Hook-Name", string(typ))
req.Header.Set("Content-Type", "application/json")
// TODO: Can we initialize this in Setup()?
// Use linear backoff strategy with the user defined values. // Use linear backoff strategy with the user defined values.
client := pester.New() client := pester.New()
client.KeepLog = true client.KeepLog = true
@ -55,24 +29,51 @@ func (h HttpHook) InvokeHook(typ HookType, info handler.HookEvent, captureOutput
return time.Duration(h.Backoff) * time.Second return time.Duration(h.Backoff) * time.Second
} }
resp, err := client.Do(req) h.client = client
return nil
}
func (h HttpHook) InvokeHook(hookReq HookRequest) (hookRes HookResponse, err error) {
jsonInfo, err := json.Marshal(hookReq)
if err != nil { if err != nil {
return nil, 0, err return hookRes, err
} }
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body) httpReq, err := http.NewRequest("POST", h.Endpoint, bytes.NewBuffer(jsonInfo))
if err != nil { if err != nil {
return nil, 0, err return hookRes, err
} }
if resp.StatusCode >= http.StatusBadRequest { for _, k := range h.ForwardHeaders {
return body, resp.StatusCode, NewHookError(fmt.Errorf("endpoint returned: %s", resp.Status), resp.StatusCode, body) // Lookup the Canonicalised version of the specified header
if vals, ok := hookReq.Event.HTTPRequest.Header[http.CanonicalHeaderKey(k)]; ok {
// but set the case specified by the user
httpReq.Header[k] = vals
}
} }
if captureOutput { httpReq.Header.Set("Content-Type", "application/json")
return body, resp.StatusCode, err
httpRes, err := h.client.Do(httpReq)
if err != nil {
return hookRes, err
}
defer httpRes.Body.Close()
httpBody, err := ioutil.ReadAll(httpRes.Body)
if err != nil {
return hookRes, err
} }
return nil, resp.StatusCode, err // Report an error, if the response has a non-2XX status code
if httpRes.StatusCode < http.StatusOK || httpRes.StatusCode >= http.StatusMultipleChoices {
return hookRes, fmt.Errorf("unexpected response code from hook endpoint (%d): %s", httpRes.StatusCode, string(httpBody))
}
if err = json.Unmarshal(httpBody, &hookRes); err != nil {
return hookRes, fmt.Errorf("failed to parse hook response: %w", err)
}
return hookRes, nil
} }

View File

@ -1,69 +1,122 @@
package hooks package hooks
import ( import (
"fmt" "log"
"plugin" "net/rpc"
"os"
"os/exec"
"github.com/tus/tusd/pkg/handler" "github.com/hashicorp/go-plugin"
) )
type PluginHookHandler interface { // TODO: When the tusd process stops, the plugin does not get properly killed
PreCreate(info handler.HookEvent) error // and lives on as a zombie process.
PostCreate(info handler.HookEvent) error
PostReceive(info handler.HookEvent) error
PostFinish(info handler.HookEvent) error
PostTerminate(info handler.HookEvent) error
PreFinish(info handler.HookEvent) error
}
type PluginHook struct { type PluginHook struct {
Path string Path string
handler PluginHookHandler handlerImpl HookHandler
} }
func (h *PluginHook) Setup() error { func (h *PluginHook) Setup() error {
p, err := plugin.Open(h.Path) // We're a host! Start by launching the plugin process.
client := plugin.NewClient(&plugin.ClientConfig{
HandshakeConfig: handshakeConfig,
Plugins: pluginMap,
Cmd: exec.Command(h.Path),
SyncStdout: os.Stdout,
SyncStderr: os.Stderr,
//Logger: logger,
})
//defer client.Kill()
// Connect via RPC
rpcClient, err := client.Client()
if err != nil { if err != nil {
log.Fatal(err)
}
// Request the plugin
raw, err := rpcClient.Dispense("hookHandler")
if err != nil {
log.Fatal(err)
}
// We should have a HookHandler now! This feels like a normal interface
// implementation but is in fact over an RPC connection.
h.handlerImpl = raw.(HookHandler)
return h.handlerImpl.Setup()
}
func (h *PluginHook) InvokeHook(req HookRequest) (HookResponse, error) {
return h.handlerImpl.InvokeHook(req)
}
// handshakeConfigs are used to just do a basic handshake between
// a plugin and host. If the handshake fails, a user friendly error is shown.
// This prevents users from executing bad plugins or executing a plugin
// directory. It is a UX feature, not a security feature.
var handshakeConfig = plugin.HandshakeConfig{
ProtocolVersion: 1,
MagicCookieKey: "TUSD_PLUGIN",
MagicCookieValue: "yes",
}
// pluginMap is the map of plugins we can dispense.
var pluginMap = map[string]plugin.Plugin{
"hookHandler": &HookHandlerPlugin{},
}
// Here is an implementation that talks over RPC
type HookHandlerRPC struct{ client *rpc.Client }
func (g *HookHandlerRPC) Setup() error {
var res interface{}
err := g.client.Call("Plugin.Setup", new(interface{}), &res)
return err return err
} }
symbol, err := p.Lookup("TusdHookHandler") func (g *HookHandlerRPC) InvokeHook(req HookRequest) (res HookResponse, err error) {
if err != nil { err = g.client.Call("Plugin.InvokeHook", req, &res)
return res, err
}
// Here is the RPC server that HookHandlerRPC talks to, conforming to
// the requirements of net/rpc
type HookHandlerRPCServer struct {
// This is the real implementation
Impl HookHandler
}
func (s *HookHandlerRPCServer) Setup(args interface{}, resp *interface{}) error {
return s.Impl.Setup()
}
func (s *HookHandlerRPCServer) InvokeHook(args HookRequest, resp *HookResponse) (err error) {
*resp, err = s.Impl.InvokeHook(args)
return err return err
} }
handler, ok := symbol.(*PluginHookHandler) // This is the implementation of plugin.Plugin so we can serve/consume this
if !ok { //
return fmt.Errorf("hooks: could not cast TusdHookHandler from %s into PluginHookHandler interface", h.Path) // This has two methods: Server must return an RPC server for this plugin
// type. We construct a HookHandlerRPCServer for this.
//
// Client must return an implementation of our interface that communicates
// over an RPC client. We return HookHandlerRPC for this.
//
// Ignore MuxBroker. That is used to create more multiplexed streams on our
// plugin connection and is a more advanced use case.
type HookHandlerPlugin struct {
// Impl Injection
Impl HookHandler
} }
h.handler = *handler func (p *HookHandlerPlugin) Server(*plugin.MuxBroker) (interface{}, error) {
return nil return &HookHandlerRPCServer{Impl: p.Impl}, nil
} }
func (h PluginHook) InvokeHook(typ HookType, info handler.HookEvent, captureOutput bool) ([]byte, int, error) { func (HookHandlerPlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) {
var err error return &HookHandlerRPC{client: c}, nil
switch typ {
case HookPostFinish:
err = h.handler.PostFinish(info)
case HookPostTerminate:
err = h.handler.PostTerminate(info)
case HookPostReceive:
err = h.handler.PostReceive(info)
case HookPostCreate:
err = h.handler.PostCreate(info)
case HookPreCreate:
err = h.handler.PreCreate(info)
case HookPreFinish:
err = h.handler.PreFinish(info)
default:
err = fmt.Errorf("hooks: unknown hook named %s", typ)
}
if err != nil {
return nil, 1, err
}
return nil, 0, nil
} }

View File

@ -1,77 +0,0 @@
// 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/ v1/hook.proto
// In addition, it may be necessary to update the protobuf or gRPC dependencies as well.
syntax = "proto3";
package v1;
import "google/protobuf/any.proto";
// Uploaded data
message Upload {
// Unique integer identifier of the uploaded file
string id = 1;
// Total file size in bytes specified in the NewUpload call
int64 Size = 2;
// Indicates whether the total file size is deferred until later
bool SizeIsDeferred = 3;
// Offset in bytes (zero-based)
int64 Offset = 4;
map<string, string> metaData = 5;
// Indicates that this is a partial upload which will later be used to form
// a final upload by concatenation. Partial uploads should not be processed
// when they are finished since they are only incomplete chunks of files.
bool isPartial = 6;
// Indicates that this is a final upload
bool isFinal = 7;
// If the upload is a final one (see IsFinal) this will be a non-empty
// ordered slice containing the ids of the uploads of which the final upload
// will consist after concatenation.
repeated string partialUploads = 8;
// Storage contains information about where the data storage saves the upload,
// for example a file path. The available values vary depending on what data
// store is used. This map may also be nil.
map <string, string> storage = 9;
}
message HTTPRequest {
// Method is the HTTP method, e.g. POST or PATCH
string method = 1;
// URI is the full HTTP request URI, e.g. /files/fooo
string uri = 2;
// RemoteAddr contains the network address that sent the request
string remoteAddr = 3;
}
// Hook's data
message Hook {
// Upload contains information about the upload that caused this hook
// to be fired.
Upload upload = 1;
// HTTPRequest contains details about the HTTP request that reached
// tusd.
HTTPRequest httpRequest = 2;
// The hook name
string name = 3;
}
// Request data to send hook
message SendRequest {
// The hook data
Hook hook = 1;
}
// Response that contains data for sended hook
message SendResponse {
// The response of the hook.
google.protobuf.Any response = 1;
}
// The hook service definition.
service HookService {
// Sends a hook
rpc Send (SendRequest) returns (SendResponse) {}
}

View File

@ -0,0 +1,116 @@
// 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
// In addition, it may be necessary to update the protobuf or gRPC dependencies as well.
syntax = "proto3";
package v2;
// HookRequest contains the information about the hook type, the involved upload,
// and causing HTTP request.
message HookRequest {
// Type is the name of the hook.
string type = 1;
// Event contains the involved upload and causing HTTP request.
Event event = 2;
}
// Event represents an event from tusd which can be handled by the application.
message Event {
// Upload contains information about the upload that caused this hook
// to be fired.
FileInfo upload = 1;
// HTTPRequest contains details about the HTTP request that reached
// tusd.
HTTPRequest httpRequest = 2;
}
// FileInfo contains information about a single upload resource.
message FileInfo {
// ID is the unique identifier of the upload resource.
string id = 1;
// Total file size in bytes specified in the NewUpload call
int64 size = 2;
// Indicates whether the total file size is deferred until later
bool sizeIsDeferred = 3;
// Offset in bytes (zero-based)
int64 offset = 4;
map<string, string> metaData = 5;
// Indicates that this is a partial upload which will later be used to form
// a final upload by concatenation. Partial uploads should not be processed
// when they are finished since they are only incomplete chunks of files.
bool isPartial = 6;
// Indicates that this is a final upload
bool isFinal = 7;
// If the upload is a final one (see IsFinal) this will be a non-empty
// ordered slice containing the ids of the uploads of which the final upload
// will consist after concatenation.
repeated string partialUploads = 8;
// Storage contains information about where the data storage saves the upload,
// for example a file path. The available values vary depending on what data
// store is used. This map may also be nil.
map <string, string> storage = 9;
}
// HTTPRequest contains basic details of an incoming HTTP request.
message HTTPRequest {
// Method is the HTTP method, e.g. POST or PATCH.
string method = 1;
// URI is the full HTTP request URI, e.g. /files/fooo.
string uri = 2;
// RemoteAddr contains the network address that sent the request.
string remoteAddr = 3;
// Header contains all HTTP headers as present in the HTTP request.
map <string, string> header = 4;
}
// HookResponse is the response after a hook is executed.
message HookResponse {
// HTTPResponse's fields can be filled to modify the HTTP response.
// This is only possible for pre-create, pre-finish and post-receive hooks.
// For other hooks this value is ignored.
// If multiple hooks modify the HTTP response, a later hook may overwrite the
// modified values from a previous hook (e.g. if multiple post-receive hooks
// are executed).
// Example usages: Send an error to the client if RejectUpload/StopUpload are
// set in the pre-create/post-receive hook. Send more information to the client
// in the pre-finish hook.
HTTPResponse httpResponse = 1;
// RejectUpload will cause the upload to be rejected and not be created during
// POST request. This value is only respected for pre-create hooks. For other hooks,
// it is ignored. Use the HTTPResponse field to send details about the rejection
// to the client.
bool rejectUpload = 2;
// 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
// to the client.
bool stopUpload = 3;
}
// HTTPResponse contains basic details of an outgoing HTTP response.
message HTTPResponse {
// StatusCode is status code, e.g. 200 or 400.
int64 statusCode = 1;
// Headers contains additional HTTP headers for the response.
map <string, string> headers = 2;
// Body is the response body.
string body = 3;
}
// The hook service definition.
service HookHandler {
// 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.
rpc InvokeHook (HookRequest) returns (HookResponse) {}
}

View File

@ -1,5 +1,7 @@
# Hooks # Hooks
TODO: Update with new details
When integrating tusd into an application, it is important to establish a communication channel between the two components. The tusd binary accomplishes this by providing a system which triggers actions when certain events happen, such as an upload being created or finished. This simple-but-powerful system enables uses ranging from logging over validation and authorization to processing the uploaded files. When integrating tusd into an application, it is important to establish a communication channel between the two components. The tusd binary accomplishes this by providing a system which triggers actions when certain events happen, such as an upload being created or finished. This simple-but-powerful system enables uses ranging from logging over validation and authorization to processing the uploaded files.
When a specific action happens during an upload (pre-create, post-receive, post-finish, or post-terminate), the hook system enables tusd to fire off a specific event. Tusd provides two ways of doing this: When a specific action happens during an upload (pre-create, post-receive, post-finish, or post-terminate), the hook system enables tusd to fire off a specific event. Tusd provides two ways of doing this:
@ -211,9 +213,9 @@ $ # Retrying 5 times with a 2 second backoff
$ tusd --hooks-http http://localhost:8081/write --hooks-http-retry 5 --hooks-http-backoff 2 $ tusd --hooks-http http://localhost:8081/write --hooks-http-retry 5 --hooks-http-backoff 2
``` ```
## GRPC Hooks ## gRPC Hooks
GRPC Hooks are the third type of hooks supported by tusd. Like the others hooks, it is disabled by default. To enable it, pass the `--hooks-grpc` option to the tusd binary. The flag's value will be a gRPC endpoint, which the tusd binary will be sent to: gRPC Hooks are the third type of hooks supported by tusd. Like the others hooks, it is disabled by default. To enable it, pass the `--hooks-grpc` option to the tusd binary. The flag's value will be a gRPC endpoint, which the tusd binary will be sent to:
```bash ```bash
$ tusd --hooks-grpc localhost:8080 $ tusd --hooks-grpc localhost:8080

3
docs/minio.txt Normal file
View File

@ -0,0 +1,3 @@
MINIO_ROOT_USER=AKIAIOSFODNN7EXAMPLE MINIO_ROOT_PASSWORD=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY ./minio server data
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY go run cmd/tusd/main.go -s3-bucket tusdtest.transloadit.com -s3-endpoint http://127.0.0.1:9000 -expose-pprof

11
examples/README.md Normal file
View File

@ -0,0 +1,11 @@
# Examples
This directory contains following examples:
- `apache2.conf` is the recommended minimum configuration for an Apache2 proxy in front of tusd.
- `nginx.conf` is the recommended minimum configuration for an Nginx proxy in front of tusd.
- `server/` is an example of how to the tusd package embedded in your own Go application.
- `hooks/file/` are Bash scripts for file hook implementations.
- `hooks/http/` is a Python HTTP server as the HTTP hook implementation.
- `hooks/grpc/` is a Python gRPC server as the gRPC hook implementation.
- `hooks/plugin/` is a Go plugin usable with the plugin hooks.

14
examples/hooks/file/post-create Executable file
View File

@ -0,0 +1,14 @@
#!/bin/sh
# This example demonstrates how to read the hook event details
# from stdout and output debug messages.
id="$TUS_ID"
size="$TUS_SIZE"
# We use >&2 to write debugging output to stderr. tusd
# will forward these to its stderr. Any output from the
# hook on stdout will be captured by tusd and interpreted
# as a response.
echo "Upload created with ID ${id} and size ${size}" >&2
cat /dev/stdin | jq . >&2

11
examples/hooks/file/post-finish Executable file
View File

@ -0,0 +1,11 @@
#!/bin/sh
# This example demonstrates how to read the hook event details
# from environment variables, stdin, and output debug messages.
# We use >&2 to write debugging output to stderr. tusd
# will forward these to its stderr. Any output from the
# hook on stdout will be captured by tusd and interpreted
# as a response.
echo "Upload $TUS_ID ($TUS_SIZE bytes) finished" >&2
cat /dev/stdin | jq . >&2

View File

@ -0,0 +1,15 @@
#!/bin/sh
# This example demonstrates how to read the hook event details
# from environment variables and output debug messages.
id="$TUS_ID"
offset="$TUS_OFFSET"
size="$TUS_SIZE"
progress=$((100 * $offset/$size))
# We use >&2 to write debugging output to stderr. tusd
# will forward these to its stderr. Any output from the
# hook on stdout will be captured by tusd and interpreted
# as a response.
echo "Upload ${id} is at ${progress}% (${offset}/${size})" >&2

View File

@ -0,0 +1,11 @@
#!/bin/sh
# This example demonstrates how to read the hook event details
# from environment variables, stdin, and output debug messages.
# We use >&2 to write debugging output to stderr. tusd
# will forward these to its stderr. Any output from the
# hook on stdout will be captured by tusd and interpreted
# as a response.
echo "Upload $TUS_ID terminated" >&2
cat /dev/stdin | jq . >&2

37
examples/hooks/file/pre-create Executable file
View File

@ -0,0 +1,37 @@
#!/bin/sh
# This example demonstrates how to read the hook event details
# from stdout, output debug messages, and reject a new upload based
# on custom constraints. Here, an upload will be rejected if the
# filename metadata is missing. Remove the following `exit 0` line
# to activate the constraint:
exit 0
hasFilename="$(cat /dev/stdin | jq '.Event.Upload.MetaData | has("filename")')"
# We use >&2 to write debugging output to stderr. tusd
# will forward these to its stderr. Any output from the
# hook on stdout will be captured by tusd and interpreted
# as a response.
echo "Filename exists: $hasFilename" >&2
if [ "$hasFilename" == "false" ]; then
# If the condition is not met, output a JSON object on stdout,
# that instructs tusd to reject the upload and respond with a custom
# HTTP error response.
cat <<END
{
"RejectUpload": true,
"HTTPResponse": {
"StatusCode": 400,
"Body": "no filename provided"
}
}
END
# It is important that the hook exits with code 0. Otherwise, tusd
# assumes the hook has failed and will print an error message about
# the hook failure.
exit 0
fi

10
examples/hooks/file/pre-finish Executable file
View File

@ -0,0 +1,10 @@
#!/bin/sh
# This example demonstrates how to read the hook event details
# from stdin, and output debug messages.
# We use >&2 to write debugging output to stderr. tusd
# will forward these to its stderr. Any output from the
# hook on stdout will be captured by tusd and interpreted
# as a response.
cat /dev/stdin | jq . >&2

View File

@ -0,0 +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=.

View File

@ -0,0 +1,139 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: hook.proto
"""Generated protocol buffer code."""
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)
_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')
_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']
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
_FILEINFO_METADATAENTRY._options = None
_FILEINFO_METADATAENTRY._serialized_options = b'8\001'
_FILEINFO_STORAGEENTRY._options = None
_FILEINFO_STORAGEENTRY._serialized_options = b'8\001'
_HTTPREQUEST_HEADERENTRY._options = None
_HTTPREQUEST_HEADERENTRY._serialized_options = b'8\001'
_HTTPRESPONSE_HEADERSENTRY._options = None
_HTTPRESPONSE_HEADERSENTRY._serialized_options = b'8\001'
_HOOKREQUEST._serialized_start=18
_HOOKREQUEST._serialized_end=71
_EVENT._serialized_start=73
_EVENT._serialized_end=148
_FILEINFO._serialized_start=151
_FILEINFO._serialized_end=474
_FILEINFO_METADATAENTRY._serialized_start=379
_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
# @@protoc_insertion_point(module_scope)

View File

@ -0,0 +1,70 @@
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
"""Client and server classes corresponding to protobuf-defined services."""
import grpc
import hook_pb2 as hook__pb2
class HookHandlerStub(object):
"""The hook service definition.
"""
def __init__(self, channel):
"""Constructor.
Args:
channel: A grpc.Channel.
"""
self.InvokeHook = channel.unary_unary(
'/v2.HookHandler/InvokeHook',
request_serializer=hook__pb2.HookRequest.SerializeToString,
response_deserializer=hook__pb2.HookResponse.FromString,
)
class HookHandlerServicer(object):
"""The hook service definition.
"""
def InvokeHook(self, request, context):
"""Sends a hook
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def add_HookHandlerServicer_to_server(servicer, server):
rpc_method_handlers = {
'InvokeHook': grpc.unary_unary_rpc_method_handler(
servicer.InvokeHook,
request_deserializer=hook__pb2.HookRequest.FromString,
response_serializer=hook__pb2.HookResponse.SerializeToString,
),
}
generic_handler = grpc.method_handlers_generic_handler(
'v2.HookHandler', rpc_method_handlers)
server.add_generic_rpc_handlers((generic_handler,))
# This class is part of an EXPERIMENTAL API.
class HookHandler(object):
"""The hook service definition.
"""
@staticmethod
def InvokeHook(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(request, target, '/v2.HookHandler/InvokeHook',
hook__pb2.HookRequest.SerializeToString,
hook__pb2.HookResponse.FromString,
options, channel_credentials,
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)

View File

@ -0,0 +1,57 @@
import grpc
from concurrent import futures
import time
import hook_pb2_grpc as pb2_grpc
import hook_pb2 as pb2
class HookHandler(pb2_grpc.HookHandlerServicer):
def __init__(self, *args, **kwargs):
pass
def InvokeHook(self, hook_request, context):
# Print data from hook request for debugging
print('Received hook request:')
print(hook_request)
# Prepare hook response structure
hook_response = pb2.HookResponse()
# 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.
if hook_request.type == 'pre-create':
filename = hook_request.event.upload.metaData['filename']
if filename == "":
hook_response.rejectUpload = True
hook_response.httpResponse.statusCode = 400
hook_response.httpResponse.body = 'no filename provided'
hook_response.httpResponse.headers['X-Some-Header'] = 'yes'
# Example: Use the post-finish hook to print information about a completed upload,
# including its storage location.
if hook_request.type == 'post-finish':
id = hook_request.event.upload.id
size = hook_request.event.upload.size
storage = hook_request.event.upload.storage
print(f'Upload {id} ({size} bytes) is finished. Find the file at:')
print(storage)
# Print data of hook response for debugging
print('Responding with hook response:')
print(hook_response)
print('------')
print('')
# Return the hook response to send back to tusd
return hook_response
def serve():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
pb2_grpc.add_HookHandlerServicer_to_server(HookHandler(), server)
server.add_insecure_port('[::]:8000')
server.start()
server.wait_for_termination()
if __name__ == '__main__':
serve()

View File

@ -0,0 +1,65 @@
from http.server import HTTPServer, BaseHTTPRequestHandler
from io import BytesIO
import json
class HTTPHookHandler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.end_headers()
self.wfile.write(b'Hello! This server only responds to POST requests')
def do_POST(self):
# Read entire body as JSON object
content_length = int(self.headers['Content-Length'])
request_body = self.rfile.read(content_length)
hook_request = json.loads(request_body)
# Print data from hook request for debugging
print('Received hook request:')
print(hook_request)
# Prepare hook response structure
hook_response = {
'HTTPResponse': {
'Headers': {}
}
}
# 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.
if hook_request['Type'] == 'pre-create':
metaData = hook_request['Event']['Upload']['MetaData']
if 'filename' not in metaData:
hook_response['RejectUpload'] = True
hook_response['HTTPResponse']['StatusCode'] = 400
hook_response['HTTPResponse']['Body'] = 'no filename provided'
hook_response['HTTPResponse']['Headers']['X-Some-Header'] = 'yes'
# Example: Use the post-finish hook to print information about a completed upload,
# including its storage location.
if hook_request['Type'] == 'post-finish':
id = hook_request['Event']['Upload']['ID']
size = hook_request['Event']['Upload']['Size']
storage = hook_request['Event']['Upload']['Storage']
print(f'Upload {id} ({size} bytes) is finished. Find the file at:')
print(storage)
# Print data of hook response for debugging
print('Responding with hook response:')
print(hook_response)
print('------')
print('')
# Send the data from the hook response as JSON output
response_body = json.dumps(hook_response)
self.send_response(200)
self.end_headers()
self.wfile.write(response_body.encode())
httpd = HTTPServer(('localhost', 8000), HTTPHookHandler)
httpd.serve_forever()

View File

@ -0,0 +1,2 @@
hook_handler: hook_handler.go
go build -o hook_handler ./hook_handler.go

View File

@ -0,0 +1,87 @@
package main
import (
"fmt"
"log"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-plugin"
"github.com/tus/tusd/cmd/tusd/cli/hooks"
)
// Here is the implementation of our hook handler
type MyHookHandler struct {
logger hclog.Logger
}
// Setup is called once the plugin has been loaded by tusd.
func (g *MyHookHandler) Setup() error {
// Use the log package or the g.logger field to write debug messages.
// Do not write to stdout directly, as this is used for communication between
// tusd and the plugin.
log.Println("MyHookHandler.Setup is invoked")
return nil
}
// InvokeHook is called for every hook that tusd fires.
func (g *MyHookHandler) InvokeHook(req hooks.HookRequest) (res hooks.HookResponse, err error) {
log.Println("MyHookHandler.InvokeHook is invoked")
// Prepare hook response structure
res.HTTPResponse.Headers = make(map[string]string)
// 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.
if req.Type == hooks.HookPreCreate {
if _, ok := req.Event.Upload.MetaData["filename"]; !ok {
res.RejectUpload = true
res.HTTPResponse.StatusCode = 400
res.HTTPResponse.Body = "no filename provided"
res.HTTPResponse.Headers["X-Some-Header"] = "yes"
}
}
// Example: Use the post-finish hook to print information about a completed upload,
// including its storage location.
if req.Type == hooks.HookPreFinish {
id := req.Event.Upload.ID
size := req.Event.Upload.Size
storage := req.Event.Upload.Storage
log.Printf("Upload %s (%d bytes) is finished. Find the file at:\n", id, size)
log.Println(storage)
}
// Return the hook response to tusd.
return res, nil
}
// handshakeConfigs are used to just do a basic handshake between
// a plugin and tusd. If the handshake fails, a user friendly error is shown.
// This prevents users from executing bad plugins or executing a plugin
// directory. It is a UX feature, not a security feature.
var handshakeConfig = plugin.HandshakeConfig{
ProtocolVersion: 1,
MagicCookieKey: "TUSD_PLUGIN",
MagicCookieValue: "yes",
}
func main() {
// 1. Initialize our handler.
myHandler := &MyHookHandler{}
// 2. Construct the plugin map. The key must be "hookHandler".
var pluginMap = map[string]plugin.Plugin{
"hookHandler": &hooks.HookHandlerPlugin{Impl: myHandler},
}
// 3. Expose the plugin to tusd.
plugin.Serve(&plugin.ServeConfig{
HandshakeConfig: handshakeConfig,
Plugins: pluginMap,
})
fmt.Println("DOONE")
}

View File

@ -1,8 +0,0 @@
#!/bin/sh
id="$TUS_ID"
offset="$TUS_OFFSET"
size="$TUS_SIZE"
echo "Upload created with ID ${id} and size ${size}"
cat /dev/stdin | jq .

View File

@ -1,4 +0,0 @@
#!/bin/sh
echo "Upload $TUS_ID ($TUS_SIZE bytes) finished"
cat /dev/stdin | jq .

View File

@ -1,8 +0,0 @@
#!/bin/sh
id="$TUS_ID"
offset="$TUS_OFFSET"
size="$TUS_SIZE"
progress=$((100 * $offset/$size))
echo "Upload ${id} is at ${progress}% (${offset}/${size})"

View File

@ -1,4 +0,0 @@
#!/bin/sh
echo "Upload $TUS_ID terminated"
cat /dev/stdin | jq .

View File

@ -1,7 +0,0 @@
#!/bin/sh
filename=$(cat /dev/stdin | jq .Upload.MetaData.filename)
if [ -z "$filename" ]; then
echo "Error: no filename provided"
exit 1
fi

1
go.mod
View File

@ -13,6 +13,7 @@ require (
github.com/golang/mock v1.6.0 github.com/golang/mock v1.6.0
github.com/golang/protobuf v1.5.2 github.com/golang/protobuf v1.5.2
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
github.com/hashicorp/go-plugin v1.4.3 // indirect
github.com/prometheus/client_golang v1.12.1 github.com/prometheus/client_golang v1.12.1
github.com/sethgrid/pester v0.0.0-20190127155807-68a33a018ad0 github.com/sethgrid/pester v0.0.0-20190127155807-68a33a018ad0
github.com/stretchr/testify v1.7.0 github.com/stretchr/testify v1.7.0

26
go.sum
View File

@ -118,6 +118,8 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.m
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk= github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk=
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
@ -219,10 +221,17 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
github.com/hashicorp/go-hclog v0.14.1 h1:nQcJDQwIAGnmoUWp8ubocEX40cCml/17YkF6csQLReU=
github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-plugin v1.4.3 h1:DXmvivbWD5qdiBts9TpBC7BYL1Aia5sxbRgQB+v6UZM=
github.com/hashicorp/go-plugin v1.4.3/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M=
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
@ -247,10 +256,17 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-ieproxy v0.0.1 h1:qiyop7gCflfhwCzGyeT0gro3sF9AIg9HU98JORTkqfI= github.com/mattn/go-ieproxy v0.0.1 h1:qiyop7gCflfhwCzGyeT0gro3sF9AIg9HU98JORTkqfI=
github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E= github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77 h1:7GoSOOW2jpsfkntVKaS2rAr1TJqfcxotyaUcuxoZSzg=
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
@ -260,6 +276,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@ -368,6 +386,7 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -443,6 +462,7 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -452,6 +472,7 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191112214154-59a1497f0cea/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191112214154-59a1497f0cea/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -617,6 +638,7 @@ google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCID
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
@ -677,6 +699,10 @@ google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEc
google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211016002631-37fc39342514 h1:Rp1vYDPD4TdkMH5S/bZbopsGCsWhPcrLBUwOVhAQCxM=
google.golang.org/genproto v0.0.0-20211016002631-37fc39342514/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=

View File

@ -41,14 +41,18 @@ type Config struct {
// response to POST requests. // response to POST requests.
RespectForwardedHeaders bool RespectForwardedHeaders bool
// PreUploadCreateCallback will be invoked before a new upload is created, if the // PreUploadCreateCallback will be invoked before a new upload is created, if the
// property is supplied. If the callback returns nil, the upload will be created. // property is supplied. If the callback returns no error, the upload will be created
// Otherwise the HTTP request will be aborted. This can be used to implement // and optional values from HTTPResponse will be contained in the HTTP response.
// validation of upload metadata etc. // If the error is non-nil, the upload will not be created. This can be used to implement
PreUploadCreateCallback func(hook HookEvent) error // 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)
// PreFinishResponseCallback will be invoked after an upload is completed but before // PreFinishResponseCallback will be invoked after an upload is completed but before
// a response is returned to the client. Error responses from the callback will be passed // a response is returned to the client. This can be used to implement post-processing validation.
// back 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.
PreFinishResponseCallback func(hook HookEvent) error // If the error is non-nil, the error will be forwarded to the client. Furthermore,
// HTTPResponse will be ignored and the error value can contain values for the HTTP response.
PreFinishResponseCallback func(hook HookEvent) (HTTPResponse, error)
} }
func (config *Config) validate() error { func (config *Config) validate() error {

View File

@ -7,7 +7,9 @@ import (
type MetaData map[string]string type MetaData map[string]string
// FileInfo contains information about a single upload resource.
type FileInfo struct { type FileInfo struct {
// ID is the unique identifier of the upload resource.
ID string ID string
// Total file size in bytes specified in the NewUpload call // Total file size in bytes specified in the NewUpload call
Size int64 Size int64
@ -41,6 +43,7 @@ type FileInfo struct {
// more data. Furthermore, a response is sent to notify the client of the // more data. Furthermore, a response is sent to notify the client of the
// interrupting and the upload is terminated (if supported by the data store), // interrupting and the upload is terminated (if supported by the data store),
// so the upload cannot be resumed anymore. // so the upload cannot be resumed anymore.
// TODO: Allow passing in a HTTP Response
func (f FileInfo) StopUpload() { func (f FileInfo) StopUpload() {
if f.stopUpload != nil { if f.stopUpload != nil {
f.stopUpload() f.stopUpload()

32
pkg/handler/error.go Normal file
View File

@ -0,0 +1,32 @@
package handler
// Error represents an error with the intent to be sent in the HTTP
// response to the client. Therefore, it also contains a HTTPResponse,
// next to an error code and error message.
type Error struct {
ErrorCode string
Message string
HTTPResponse HTTPResponse
}
func (e Error) Error() string {
return e.ErrorCode + ": " + e.Message
}
// NewError constructs a new Error object with the given error code and message.
// The corresponding HTTP response will have the provided status code
// and a body consisting of the error details.
// responses. See the net/http package for standardized status codes.
func NewError(errCode string, message string, statusCode int) Error {
return Error{
ErrorCode: errCode,
Message: message,
HTTPResponse: HTTPResponse{
StatusCode: statusCode,
Body: errCode + ": " + message + "\n",
Headers: HTTPHeaders{
"Content-Type": "text/plain; charset=utf-8",
},
},
}
}

View File

@ -77,9 +77,7 @@ func TestHead(t *testing.T) {
"Tus-Resumable": "1.0.0", "Tus-Resumable": "1.0.0",
}, },
Code: http.StatusNotFound, Code: http.StatusNotFound,
ResHeader: map[string]string{ ResHeader: map[string]string{},
"Content-Length": "0",
},
}).Run(handler, t) }).Run(handler, t)
if res.Body.String() != "" { if res.Body.String() != "" {

25
pkg/handler/hooks.go Normal file
View File

@ -0,0 +1,25 @@
package handler
import "net/http"
// HookEvent represents an event from tusd which can be handled by the application.
type HookEvent struct {
// Upload contains information about the upload that caused this hook
// to be fired.
Upload FileInfo
// HTTPRequest contains details about the HTTP request that reached
// tusd.
HTTPRequest HTTPRequest
}
func newHookEvent(info FileInfo, r *http.Request) HookEvent {
return HookEvent{
Upload: info,
HTTPRequest: HTTPRequest{
Method: r.Method,
URI: r.RequestURI,
RemoteAddr: r.RemoteAddr,
Header: r.Header,
},
}
}

80
pkg/handler/http.go Normal file
View File

@ -0,0 +1,80 @@
package handler
import (
"net/http"
"strconv"
)
// HTTPRequest contains basic details of an incoming HTTP request.
type HTTPRequest struct {
// Method is the HTTP method, e.g. POST or PATCH.
Method string
// URI is the full HTTP request URI, e.g. /files/fooo.
URI string
// RemoteAddr contains the network address that sent the request.
RemoteAddr string
// Header contains all HTTP headers as present in the HTTP request.
Header http.Header
}
type HTTPHeaders map[string]string
// HTTPResponse contains basic details of an outgoing HTTP response.
type HTTPResponse struct {
// StatusCode is status code, e.g. 200 or 400.
StatusCode int
// Body is the response body.
Body string
// Headers contains additional HTTP headers for the response.
// TODO: Uniform naming with HTTPRequest.Header
Headers HTTPHeaders
}
// writeTo writes the HTTP response into w, as specified by the fields in resp.
func (resp HTTPResponse) writeTo(w http.ResponseWriter) {
headers := w.Header()
for key, value := range resp.Headers {
headers.Set(key, value)
}
if len(resp.Body) > 0 {
headers.Set("Content-Length", strconv.Itoa(len(resp.Body)))
}
w.WriteHeader(resp.StatusCode)
if len(resp.Body) > 0 {
w.Write([]byte(resp.Body))
}
}
// MergeWith returns a copy of resp1, where non-default values from resp2 overwrite
// values from resp1.
func (resp1 HTTPResponse) MergeWith(resp2 HTTPResponse) HTTPResponse {
// Clone the response 1 and use it as a basis
newResp := resp1
// Take the status code and body from response 2 to
// overwrite values from response 1.
if resp2.StatusCode != 0 {
newResp.StatusCode = resp2.StatusCode
}
if len(resp2.Body) > 0 {
newResp.Body = resp2.Body
}
// For the headers, me must make a new map to avoid writing
// into the header map from response 1.
newResp.Headers = make(HTTPHeaders, len(resp1.Headers)+len(resp2.Headers))
for key, value := range resp1.Headers {
newResp.Headers[key] = value
}
for key, value := range resp2.Headers {
newResp.Headers[key] = value
}
return newResp
}

View File

@ -31,7 +31,7 @@ func (m Metrics) incRequestsTotal(method string) {
// TODO: Rework to only store error code // TODO: Rework to only store error code
// incErrorsTotal increases the counter for this error atomically by one. // incErrorsTotal increases the counter for this error atomically by one.
func (m Metrics) incErrorsTotal(err HTTPError) { func (m Metrics) incErrorsTotal(err Error) {
ptr := m.ErrorsTotal.retrievePointerFor(err) ptr := m.ErrorsTotal.retrievePointerFor(err)
atomic.AddUint64(ptr, 1) atomic.AddUint64(ptr, 1)
} }
@ -95,10 +95,10 @@ func newErrorsTotalMap() *ErrorsTotalMap {
// retrievePointerFor returns (after creating it if necessary) the pointer to // retrievePointerFor returns (after creating it if necessary) the pointer to
// the counter for the error. // the counter for the error.
func (e *ErrorsTotalMap) retrievePointerFor(err HTTPError) *uint64 { func (e *ErrorsTotalMap) retrievePointerFor(err Error) *uint64 {
serr := ErrorsTotalMapEntry{ serr := ErrorsTotalMapEntry{
ErrorCode: err.ErrorCode(), ErrorCode: err.ErrorCode,
StatusCode: err.StatusCode(), StatusCode: err.HTTPResponse.StatusCode,
} }
e.lock.RLock() e.lock.RLock()

View File

@ -23,103 +23,31 @@ var (
reMimeType = regexp.MustCompile(`^[a-z]+\/[a-z0-9\-\+\.]+$`) reMimeType = regexp.MustCompile(`^[a-z]+\/[a-z0-9\-\+\.]+$`)
) )
// HTTPError represents an error with an additional status code attached
// which may be used when this error is sent in a HTTP response.
// See the net/http package for standardized status codes.
type HTTPError interface {
error
ErrorCode() string
StatusCode() int
Body() []byte
}
type httpError struct {
errorCode string
message string
statusCode int
}
func (err httpError) Error() string {
return err.errorCode + ": " + err.message
}
func (err httpError) StatusCode() int {
return err.statusCode
}
func (err httpError) ErrorCode() string {
return err.errorCode
}
func (err httpError) Body() []byte {
return []byte(err.Error())
}
// NewHTTPError adds the given status code to the provided error and returns
// the new error instance. The status code may be used in corresponding HTTP
// responses. See the net/http package for standardized status codes.
func NewHTTPError(errCode string, message string, statusCode int) HTTPError {
return httpError{errCode, message, statusCode}
}
var ( var (
ErrUnsupportedVersion = NewHTTPError("ERR_UNSUPPORTED_VERSION", "missing, invalid or unsupported Tus-Resumable header", http.StatusPreconditionFailed) ErrUnsupportedVersion = NewError("ERR_UNSUPPORTED_VERSION", "missing, invalid or unsupported Tus-Resumable header", http.StatusPreconditionFailed)
ErrMaxSizeExceeded = NewHTTPError("ERR_MAX_SIZE_EXCEEDED", "maximum size exceeded", http.StatusRequestEntityTooLarge) ErrMaxSizeExceeded = NewError("ERR_MAX_SIZE_EXCEEDED", "maximum size exceeded", http.StatusRequestEntityTooLarge)
ErrInvalidContentType = NewHTTPError("ERR_INVALID_CONTENT_TYPE", "missing or invalid Content-Type header", http.StatusBadRequest) ErrInvalidContentType = NewError("ERR_INVALID_CONTENT_TYPE", "missing or invalid Content-Type header", http.StatusBadRequest)
ErrInvalidUploadLength = NewHTTPError("ERR_INVALID_UPLOAD_LENGTH", "missing or invalid Upload-Length header", http.StatusBadRequest) ErrInvalidUploadLength = NewError("ERR_INVALID_UPLOAD_LENGTH", "missing or invalid Upload-Length header", http.StatusBadRequest)
ErrInvalidOffset = NewHTTPError("ERR_INVALID_OFFSET", "missing or invalid Upload-Offset header", http.StatusBadRequest) ErrInvalidOffset = NewError("ERR_INVALID_OFFSET", "missing or invalid Upload-Offset header", http.StatusBadRequest)
ErrNotFound = NewHTTPError("ERR_UPLOAD_NOT_FOUND", "upload not found", http.StatusNotFound) ErrNotFound = NewError("ERR_UPLOAD_NOT_FOUND", "upload not found", http.StatusNotFound)
ErrFileLocked = NewHTTPError("ERR_UPLOAD_LOCKED", "file currently locked", http.StatusLocked) ErrFileLocked = NewError("ERR_UPLOAD_LOCKED", "file currently locked", http.StatusLocked)
ErrMismatchOffset = NewHTTPError("ERR_MISMATCHED_OFFSET", "mismatched offset", http.StatusConflict) ErrMismatchOffset = NewError("ERR_MISMATCHED_OFFSET", "mismatched offset", http.StatusConflict)
ErrSizeExceeded = NewHTTPError("ERR_UPLOAD_SIZE_EXCEEDED", "upload's size exceeded", http.StatusRequestEntityTooLarge) ErrSizeExceeded = NewError("ERR_UPLOAD_SIZE_EXCEEDED", "upload's size exceeded", http.StatusRequestEntityTooLarge)
ErrNotImplemented = NewHTTPError("ERR_NOT_IMPLEMENTED", "feature not implemented", http.StatusNotImplemented) ErrNotImplemented = NewError("ERR_NOT_IMPLEMENTED", "feature not implemented", http.StatusNotImplemented)
ErrUploadNotFinished = NewHTTPError("ERR_UPLOAD_NOT_FINISHED", "one of the partial uploads is not finished", http.StatusBadRequest) ErrUploadNotFinished = NewError("ERR_UPLOAD_NOT_FINISHED", "one of the partial uploads is not finished", http.StatusBadRequest)
ErrInvalidConcat = NewHTTPError("ERR_INVALID_CONCAT", "invalid Upload-Concat header", http.StatusBadRequest) ErrInvalidConcat = NewError("ERR_INVALID_CONCAT", "invalid Upload-Concat header", http.StatusBadRequest)
ErrModifyFinal = NewHTTPError("ERR_MODIFY_FINAL", "modifying a final upload is not allowed", http.StatusForbidden) ErrModifyFinal = NewError("ERR_MODIFY_FINAL", "modifying a final upload is not allowed", http.StatusForbidden)
ErrUploadLengthAndUploadDeferLength = NewHTTPError("ERR_AMBIGUOUS_UPLOAD_LENGTH", "provided both Upload-Length and Upload-Defer-Length", http.StatusBadRequest) ErrUploadLengthAndUploadDeferLength = NewError("ERR_AMBIGUOUS_UPLOAD_LENGTH", "provided both Upload-Length and Upload-Defer-Length", http.StatusBadRequest)
ErrInvalidUploadDeferLength = NewHTTPError("ERR_INVALID_UPLOAD_LENGTH_DEFER", "invalid Upload-Defer-Length header", http.StatusBadRequest) ErrInvalidUploadDeferLength = NewError("ERR_INVALID_UPLOAD_LENGTH_DEFER", "invalid Upload-Defer-Length header", http.StatusBadRequest)
ErrUploadStoppedByServer = NewHTTPError("ERR_UPLOAD_STOPPED", "upload has been stopped by server", http.StatusBadRequest) ErrUploadStoppedByServer = NewError("ERR_UPLOAD_STOPPED", "upload has been stopped by server", http.StatusBadRequest)
ErrUploadRejectedByServer = NewError("ERR_UPLOAD_REJECTED", "upload creation has been rejected by server", http.StatusBadRequest)
// TODO: These two responses are 500 for backwards compatability. We should discuss // TODO: These two responses are 500 for backwards compatability. We should discuss
// whether it is better to more them to 4XX status codes. // whether it is better to more them to 4XX status codes.
ErrReadTimeout = NewHTTPError("ERR_READ_TIMEOUT", "timeout while reading request body", http.StatusInternalServerError) ErrReadTimeout = NewError("ERR_READ_TIMEOUT", "timeout while reading request body", http.StatusInternalServerError)
ErrConnectionReset = NewHTTPError("ERR_CONNECTION_RESET", "TCP connection reset by peer", http.StatusInternalServerError) ErrConnectionReset = NewError("ERR_CONNECTION_RESET", "TCP connection reset by peer", http.StatusInternalServerError)
) )
// HTTPRequest contains basic details of an incoming HTTP request.
type HTTPRequest struct {
// Method is the HTTP method, e.g. POST or PATCH
Method string
// URI is the full HTTP request URI, e.g. /files/fooo
URI string
// RemoteAddr contains the network address that sent the request
RemoteAddr string
// Header contains all HTTP headers as present in the HTTP request.
Header http.Header
}
// HookEvent represents an event from tusd which can be handled by the application.
type HookEvent struct {
// Upload contains information about the upload that caused this hook
// to be fired.
Upload FileInfo
// HTTPRequest contains details about the HTTP request that reached
// tusd.
HTTPRequest HTTPRequest
}
func newHookEvent(info FileInfo, r *http.Request) HookEvent {
return HookEvent{
Upload: info,
HTTPRequest: HTTPRequest{
Method: r.Method,
URI: r.RequestURI,
RemoteAddr: r.RemoteAddr,
Header: r.Header,
},
}
}
// UnroutedHandler exposes methods to handle requests as part of the tus protocol, // UnroutedHandler exposes methods to handle requests as part of the tus protocol,
// such as PostFile, HeadFile, PatchFile and DelFile. In addition the GetFile method // such as PostFile, HeadFile, PatchFile and DelFile. In addition the GetFile method
// is provided which is, however, not part of the specification. // is provided which is, however, not part of the specification.
@ -266,7 +194,9 @@ func (handler *UnroutedHandler) Middleware(h http.Handler) http.Handler {
// will be ignored or interpreted as a rejection. // will be ignored or interpreted as a rejection.
// For example, the Presto engine, which is used in older versions of // For example, the Presto engine, which is used in older versions of
// Opera, Opera Mobile and Opera Mini, handles CORS this way. // Opera, Opera Mobile and Opera Mini, handles CORS this way.
handler.sendResp(w, r, http.StatusOK) handler.sendResp(w, r, HTTPResponse{
StatusCode: http.StatusOK,
})
return return
} }
@ -353,11 +283,18 @@ func (handler *UnroutedHandler) PostFile(w http.ResponseWriter, r *http.Request)
PartialUploads: partialUploadIDs, PartialUploads: partialUploadIDs,
} }
resp := HTTPResponse{
StatusCode: http.StatusCreated,
Headers: HTTPHeaders{},
}
if handler.config.PreUploadCreateCallback != nil { if handler.config.PreUploadCreateCallback != nil {
if err := handler.config.PreUploadCreateCallback(newHookEvent(info, r)); err != nil { resp2, err := handler.config.PreUploadCreateCallback(newHookEvent(info, r))
if err != nil {
handler.sendError(w, r, err) handler.sendError(w, r, err)
return return
} }
resp = resp.MergeWith(resp2)
} }
upload, err := handler.composer.Core.NewUpload(ctx, info) upload, err := handler.composer.Core.NewUpload(ctx, info)
@ -377,7 +314,7 @@ func (handler *UnroutedHandler) PostFile(w http.ResponseWriter, r *http.Request)
// Add the Location header directly after creating the new resource to even // Add the Location header directly after creating the new resource to even
// include it in cases of failure when an error is returned // include it in cases of failure when an error is returned
url := handler.absFileURL(r, id) url := handler.absFileURL(r, id)
w.Header().Set("Location", url) resp.Headers["Location"] = url
handler.Metrics.incUploadsCreated() handler.Metrics.incUploadsCreated()
handler.log("UploadCreated", "id", id, "size", i64toa(size), "url", url) handler.log("UploadCreated", "id", id, "size", i64toa(size), "url", url)
@ -410,7 +347,8 @@ func (handler *UnroutedHandler) PostFile(w http.ResponseWriter, r *http.Request)
defer lock.Unlock() defer lock.Unlock()
} }
if err := handler.writeChunk(ctx, upload, info, w, r); err != nil { resp, err = handler.writeChunk(ctx, upload, info, resp, r)
if err != nil {
handler.sendError(w, r, err) handler.sendError(w, r, err)
return return
} }
@ -418,13 +356,14 @@ func (handler *UnroutedHandler) PostFile(w http.ResponseWriter, r *http.Request)
// Directly finish the upload if the upload is empty (i.e. has a size of 0). // Directly finish the upload if the upload is empty (i.e. has a size of 0).
// This statement is in an else-if block to avoid causing duplicate calls // This statement is in an else-if block to avoid causing duplicate calls
// to finishUploadIfComplete if an upload is empty and contains a chunk. // to finishUploadIfComplete if an upload is empty and contains a chunk.
if err := handler.finishUploadIfComplete(ctx, upload, info, r); err != nil { resp, err = handler.finishUploadIfComplete(ctx, upload, info, resp, r)
if err != nil {
handler.sendError(w, r, err) handler.sendError(w, r, err)
return return
} }
} }
handler.sendResp(w, r, http.StatusCreated) handler.sendResp(w, r, resp)
} }
// HeadFile returns the length and offset for the HEAD request // HeadFile returns the length and offset for the HEAD request
@ -459,9 +398,14 @@ func (handler *UnroutedHandler) HeadFile(w http.ResponseWriter, r *http.Request)
return return
} }
resp := HTTPResponse{
StatusCode: http.StatusOK,
Headers: make(HTTPHeaders),
}
// Add Upload-Concat header if possible // Add Upload-Concat header if possible
if info.IsPartial { if info.IsPartial {
w.Header().Set("Upload-Concat", "partial") resp.Headers["Upload-Concat"] = "partial"
} }
if info.IsFinal { if info.IsFinal {
@ -472,23 +416,23 @@ func (handler *UnroutedHandler) HeadFile(w http.ResponseWriter, r *http.Request)
// Remove trailing space // Remove trailing space
v = v[:len(v)-1] v = v[:len(v)-1]
w.Header().Set("Upload-Concat", v) resp.Headers["Upload-Concat"] = v
} }
if len(info.MetaData) != 0 { if len(info.MetaData) != 0 {
w.Header().Set("Upload-Metadata", SerializeMetadataHeader(info.MetaData)) resp.Headers["Upload-Metadata"] = SerializeMetadataHeader(info.MetaData)
} }
if info.SizeIsDeferred { if info.SizeIsDeferred {
w.Header().Set("Upload-Defer-Length", UploadLengthDeferred) resp.Headers["Upload-Defer-Length"] = UploadLengthDeferred
} else { } else {
w.Header().Set("Upload-Length", strconv.FormatInt(info.Size, 10)) resp.Headers["Upload-Length"] = strconv.FormatInt(info.Size, 10)
w.Header().Set("Content-Length", strconv.FormatInt(info.Size, 10)) resp.Headers["Content-Length"] = strconv.FormatInt(info.Size, 10)
} }
w.Header().Set("Cache-Control", "no-store") resp.Headers["Cache-Control"] = "no-store"
w.Header().Set("Upload-Offset", strconv.FormatInt(info.Offset, 10)) resp.Headers["Upload-Offset"] = strconv.FormatInt(info.Offset, 10)
handler.sendResp(w, r, http.StatusOK) handler.sendResp(w, r, resp)
} }
// PatchFile adds a chunk to an upload. This operation is only allowed // PatchFile adds a chunk to an upload. This operation is only allowed
@ -548,10 +492,15 @@ func (handler *UnroutedHandler) PatchFile(w http.ResponseWriter, r *http.Request
return return
} }
resp := HTTPResponse{
StatusCode: http.StatusNoContent,
Headers: make(HTTPHeaders, 1), // Initialize map, so writeChunk can set the Upload-Offset header.
}
// Do not proxy the call to the data store if the upload is already completed // Do not proxy the call to the data store if the upload is already completed
if !info.SizeIsDeferred && info.Offset == info.Size { if !info.SizeIsDeferred && info.Offset == info.Size {
w.Header().Set("Upload-Offset", strconv.FormatInt(offset, 10)) resp.Headers["Upload-Offset"] = strconv.FormatInt(offset, 10)
handler.sendResp(w, r, http.StatusNoContent) handler.sendResp(w, r, resp)
return return
} }
@ -580,18 +529,19 @@ func (handler *UnroutedHandler) PatchFile(w http.ResponseWriter, r *http.Request
info.SizeIsDeferred = false info.SizeIsDeferred = false
} }
if err := handler.writeChunk(ctx, upload, info, w, r); err != nil { resp, err = handler.writeChunk(ctx, upload, info, resp, r)
if err != nil {
handler.sendError(w, r, err) handler.sendError(w, r, err)
return return
} }
handler.sendResp(w, r, http.StatusNoContent) handler.sendResp(w, r, resp)
} }
// writeChunk reads the body from the requests r and appends it to the upload // writeChunk reads the body from the requests r and appends it to the upload
// with the corresponding id. Afterwards, it will set the necessary response // with the corresponding id. Afterwards, it will set the necessary response
// headers but will not send the response. // headers but will not send the response.
func (handler *UnroutedHandler) writeChunk(ctx context.Context, upload Upload, info FileInfo, w http.ResponseWriter, r *http.Request) error { func (handler *UnroutedHandler) writeChunk(ctx context.Context, upload Upload, info FileInfo, resp HTTPResponse, r *http.Request) (HTTPResponse, error) {
// Get Content-Length if possible // Get Content-Length if possible
length := r.ContentLength length := r.ContentLength
offset := info.Offset offset := info.Offset
@ -599,7 +549,7 @@ func (handler *UnroutedHandler) writeChunk(ctx context.Context, upload Upload, i
// Test if this upload fits into the file's size // Test if this upload fits into the file's size
if !info.SizeIsDeferred && offset+length > info.Size { if !info.SizeIsDeferred && offset+length > info.Size {
return ErrSizeExceeded return resp, ErrSizeExceeded
} }
maxSize := info.Size - offset maxSize := info.Size - offset
@ -679,27 +629,27 @@ func (handler *UnroutedHandler) writeChunk(ctx context.Context, upload Upload, i
handler.log("ChunkWriteComplete", "id", id, "bytesWritten", i64toa(bytesWritten)) handler.log("ChunkWriteComplete", "id", id, "bytesWritten", i64toa(bytesWritten))
if err != nil { if err != nil {
return err return resp, err
} }
// Send new offset to client // Send new offset to client
newOffset := offset + bytesWritten newOffset := offset + bytesWritten
w.Header().Set("Upload-Offset", strconv.FormatInt(newOffset, 10)) resp.Headers["Upload-Offset"] = strconv.FormatInt(newOffset, 10)
handler.Metrics.incBytesReceived(uint64(bytesWritten)) handler.Metrics.incBytesReceived(uint64(bytesWritten))
info.Offset = newOffset info.Offset = newOffset
return handler.finishUploadIfComplete(ctx, upload, info, r) return handler.finishUploadIfComplete(ctx, upload, info, resp, r)
} }
// finishUploadIfComplete checks whether an upload is completed (i.e. upload offset // finishUploadIfComplete checks whether an upload is completed (i.e. upload offset
// matches upload size) and if so, it will call the data store's FinishUpload // matches upload size) and if so, it will call the data store's FinishUpload
// function and send the necessary message on the CompleteUpload channel. // function and send the necessary message on the CompleteUpload channel.
func (handler *UnroutedHandler) finishUploadIfComplete(ctx context.Context, upload Upload, info FileInfo, r *http.Request) error { func (handler *UnroutedHandler) finishUploadIfComplete(ctx context.Context, upload Upload, info FileInfo, resp HTTPResponse, r *http.Request) (HTTPResponse, error) {
// If the upload is completed, ... // If the upload is completed, ...
if !info.SizeIsDeferred && info.Offset == info.Size { if !info.SizeIsDeferred && info.Offset == info.Size {
// ... allow custom mechanism to finish and cleanup the upload // ... allow custom mechanism to finish and cleanup the upload
if err := upload.FinishUpload(ctx); err != nil { if err := upload.FinishUpload(ctx); err != nil {
return err return resp, err
} }
// ... send the info out to the channel // ... send the info out to the channel
@ -710,13 +660,15 @@ func (handler *UnroutedHandler) finishUploadIfComplete(ctx context.Context, uplo
handler.Metrics.incUploadsFinished() handler.Metrics.incUploadsFinished()
if handler.config.PreFinishResponseCallback != nil { if handler.config.PreFinishResponseCallback != nil {
if err := handler.config.PreFinishResponseCallback(newHookEvent(info, r)); err != nil { resp2, err := handler.config.PreFinishResponseCallback(newHookEvent(info, r))
return err if err != nil {
return resp, err
} }
resp = resp.MergeWith(resp2)
} }
} }
return nil return resp, nil
} }
// GetFile handles requests to download a file using a GET request. This is not // GetFile handles requests to download a file using a GET request. This is not
@ -752,16 +704,21 @@ func (handler *UnroutedHandler) GetFile(w http.ResponseWriter, r *http.Request)
return return
} }
// Set headers before sending responses
w.Header().Set("Content-Length", strconv.FormatInt(info.Offset, 10))
contentType, contentDisposition := filterContentType(info) contentType, contentDisposition := filterContentType(info)
w.Header().Set("Content-Type", contentType) resp := HTTPResponse{
w.Header().Set("Content-Disposition", contentDisposition) StatusCode: http.StatusOK,
Headers: HTTPHeaders{
"Content-Length": strconv.FormatInt(info.Offset, 10),
"Content-Type": contentType,
"Content-Disposition": contentDisposition,
},
Body: "", // Body is intentionally left empty, and we copy it manually in later.
}
// If no data has been uploaded yet, respond with an empty "204 No Content" status. // If no data has been uploaded yet, respond with an empty "204 No Content" status.
if info.Offset == 0 { if info.Offset == 0 {
handler.sendResp(w, r, http.StatusNoContent) resp.StatusCode = http.StatusNoContent
handler.sendResp(w, r, resp)
return return
} }
@ -771,7 +728,7 @@ func (handler *UnroutedHandler) GetFile(w http.ResponseWriter, r *http.Request)
return return
} }
handler.sendResp(w, r, http.StatusOK) handler.sendResp(w, r, resp)
io.Copy(w, src) io.Copy(w, src)
// Try to close the reader if the io.Closer interface is implemented // Try to close the reader if the io.Closer interface is implemented
@ -888,7 +845,9 @@ func (handler *UnroutedHandler) DelFile(w http.ResponseWriter, r *http.Request)
return return
} }
handler.sendResp(w, r, http.StatusNoContent) handler.sendResp(w, r, HTTPResponse{
StatusCode: http.StatusNoContent,
})
} }
// terminateUpload passes a given upload to the DataStore's Terminater, // terminateUpload passes a given upload to the DataStore's Terminater,
@ -950,33 +909,27 @@ func (handler *UnroutedHandler) sendError(w http.ResponseWriter, r *http.Request
// err = nil // err = nil
//} //}
statusErr, ok := err.(HTTPError) detailedErr, ok := err.(Error)
if !ok { if !ok {
handler.log("InternalServerError", "message", err.Error(), "method", r.Method, "path", r.URL.Path, "requestId", getRequestId(r)) handler.log("InternalServerError", "message", err.Error(), "method", r.Method, "path", r.URL.Path, "requestId", getRequestId(r))
statusErr = NewHTTPError("ERR_INTERNAL_SERVER_ERROR", err.Error(), http.StatusInternalServerError) detailedErr = NewError("ERR_INTERNAL_SERVER_ERROR", err.Error(), http.StatusInternalServerError)
} }
reason := append(statusErr.Body(), '\n') // If we are sending the response for a HEAD request, ensure that we are not including
// any response body.
if r.Method == "HEAD" { if r.Method == "HEAD" {
reason = nil detailedErr.HTTPResponse.Body = ""
} }
// TODO: Allow JSON response handler.sendResp(w, r, detailedErr.HTTPResponse)
w.Header().Set("Content-Type", "text/plain; charset=utf-8") handler.Metrics.incErrorsTotal(detailedErr)
w.Header().Set("Content-Length", strconv.Itoa(len(reason)))
w.WriteHeader(statusErr.StatusCode())
w.Write(reason)
handler.log("ResponseOutgoing", "status", strconv.Itoa(statusErr.StatusCode()), "method", r.Method, "path", r.URL.Path, "error", statusErr.ErrorCode(), "requestId", getRequestId(r))
handler.Metrics.incErrorsTotal(statusErr)
} }
// sendResp writes the header to w with the specified status code. // sendResp writes the header to w with the specified status code.
func (handler *UnroutedHandler) sendResp(w http.ResponseWriter, r *http.Request, status int) { func (handler *UnroutedHandler) sendResp(w http.ResponseWriter, r *http.Request, resp HTTPResponse) {
w.WriteHeader(status) resp.writeTo(w)
handler.log("ResponseOutgoing", "status", strconv.Itoa(status), "method", r.Method, "path", r.URL.Path, "requestId", getRequestId(r)) handler.log("ResponseOutgoing", "status", strconv.Itoa(resp.StatusCode), "method", r.Method, "path", r.URL.Path, "requestId", getRequestId(r), "body", resp.Body)
} }
// Make an absolute URLs to the given upload id. If the base path is absolute // Make an absolute URLs to the given upload id. If the base path is absolute

View File

@ -1,475 +0,0 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: v1/hook.proto
package v1
import (
context "context"
fmt "fmt"
proto "github.com/golang/protobuf/proto"
any "github.com/golang/protobuf/ptypes/any"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
math "math"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
// Uploaded data
type Upload struct {
// Unique integer identifier of the uploaded file
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
// Total file size in bytes specified in the NewUpload call
Size int64 `protobuf:"varint,2,opt,name=Size,proto3" json:"Size,omitempty"`
// Indicates whether the total file size is deferred until later
SizeIsDeferred bool `protobuf:"varint,3,opt,name=SizeIsDeferred,proto3" json:"SizeIsDeferred,omitempty"`
// Offset in bytes (zero-based)
Offset int64 `protobuf:"varint,4,opt,name=Offset,proto3" json:"Offset,omitempty"`
MetaData map[string]string `protobuf:"bytes,5,rep,name=metaData,proto3" json:"metaData,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
// Indicates that this is a partial upload which will later be used to form
// a final upload by concatenation. Partial uploads should not be processed
// when they are finished since they are only incomplete chunks of files.
IsPartial bool `protobuf:"varint,6,opt,name=isPartial,proto3" json:"isPartial,omitempty"`
// Indicates that this is a final upload
IsFinal bool `protobuf:"varint,7,opt,name=isFinal,proto3" json:"isFinal,omitempty"`
// If the upload is a final one (see IsFinal) this will be a non-empty
// ordered slice containing the ids of the uploads of which the final upload
// will consist after concatenation.
PartialUploads []string `protobuf:"bytes,8,rep,name=partialUploads,proto3" json:"partialUploads,omitempty"`
// Storage contains information about where the data storage saves the upload,
// for example a file path. The available values vary depending on what data
// store is used. This map may also be nil.
Storage map[string]string `protobuf:"bytes,9,rep,name=storage,proto3" json:"storage,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *Upload) Reset() { *m = Upload{} }
func (m *Upload) String() string { return proto.CompactTextString(m) }
func (*Upload) ProtoMessage() {}
func (*Upload) Descriptor() ([]byte, []int) {
return fileDescriptor_581082325ef044c1, []int{0}
}
func (m *Upload) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_Upload.Unmarshal(m, b)
}
func (m *Upload) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_Upload.Marshal(b, m, deterministic)
}
func (m *Upload) XXX_Merge(src proto.Message) {
xxx_messageInfo_Upload.Merge(m, src)
}
func (m *Upload) XXX_Size() int {
return xxx_messageInfo_Upload.Size(m)
}
func (m *Upload) XXX_DiscardUnknown() {
xxx_messageInfo_Upload.DiscardUnknown(m)
}
var xxx_messageInfo_Upload proto.InternalMessageInfo
func (m *Upload) GetId() string {
if m != nil {
return m.Id
}
return ""
}
func (m *Upload) GetSize() int64 {
if m != nil {
return m.Size
}
return 0
}
func (m *Upload) GetSizeIsDeferred() bool {
if m != nil {
return m.SizeIsDeferred
}
return false
}
func (m *Upload) GetOffset() int64 {
if m != nil {
return m.Offset
}
return 0
}
func (m *Upload) GetMetaData() map[string]string {
if m != nil {
return m.MetaData
}
return nil
}
func (m *Upload) GetIsPartial() bool {
if m != nil {
return m.IsPartial
}
return false
}
func (m *Upload) GetIsFinal() bool {
if m != nil {
return m.IsFinal
}
return false
}
func (m *Upload) GetPartialUploads() []string {
if m != nil {
return m.PartialUploads
}
return nil
}
func (m *Upload) GetStorage() map[string]string {
if m != nil {
return m.Storage
}
return nil
}
type HTTPRequest struct {
// Method is the HTTP method, e.g. POST or PATCH
Method string `protobuf:"bytes,1,opt,name=method,proto3" json:"method,omitempty"`
// URI is the full HTTP request URI, e.g. /files/fooo
Uri string `protobuf:"bytes,2,opt,name=uri,proto3" json:"uri,omitempty"`
// RemoteAddr contains the network address that sent the request
RemoteAddr string `protobuf:"bytes,3,opt,name=remoteAddr,proto3" json:"remoteAddr,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *HTTPRequest) Reset() { *m = HTTPRequest{} }
func (m *HTTPRequest) String() string { return proto.CompactTextString(m) }
func (*HTTPRequest) ProtoMessage() {}
func (*HTTPRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_581082325ef044c1, []int{1}
}
func (m *HTTPRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_HTTPRequest.Unmarshal(m, b)
}
func (m *HTTPRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_HTTPRequest.Marshal(b, m, deterministic)
}
func (m *HTTPRequest) XXX_Merge(src proto.Message) {
xxx_messageInfo_HTTPRequest.Merge(m, src)
}
func (m *HTTPRequest) XXX_Size() int {
return xxx_messageInfo_HTTPRequest.Size(m)
}
func (m *HTTPRequest) XXX_DiscardUnknown() {
xxx_messageInfo_HTTPRequest.DiscardUnknown(m)
}
var xxx_messageInfo_HTTPRequest proto.InternalMessageInfo
func (m *HTTPRequest) GetMethod() string {
if m != nil {
return m.Method
}
return ""
}
func (m *HTTPRequest) GetUri() string {
if m != nil {
return m.Uri
}
return ""
}
func (m *HTTPRequest) GetRemoteAddr() string {
if m != nil {
return m.RemoteAddr
}
return ""
}
// Hook's data
type Hook struct {
// Upload contains information about the upload that caused this hook
// to be fired.
Upload *Upload `protobuf:"bytes,1,opt,name=upload,proto3" json:"upload,omitempty"`
// HTTPRequest contains details about the HTTP request that reached
// tusd.
HttpRequest *HTTPRequest `protobuf:"bytes,2,opt,name=httpRequest,proto3" json:"httpRequest,omitempty"`
// The hook name
Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *Hook) Reset() { *m = Hook{} }
func (m *Hook) String() string { return proto.CompactTextString(m) }
func (*Hook) ProtoMessage() {}
func (*Hook) Descriptor() ([]byte, []int) {
return fileDescriptor_581082325ef044c1, []int{2}
}
func (m *Hook) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_Hook.Unmarshal(m, b)
}
func (m *Hook) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_Hook.Marshal(b, m, deterministic)
}
func (m *Hook) XXX_Merge(src proto.Message) {
xxx_messageInfo_Hook.Merge(m, src)
}
func (m *Hook) XXX_Size() int {
return xxx_messageInfo_Hook.Size(m)
}
func (m *Hook) XXX_DiscardUnknown() {
xxx_messageInfo_Hook.DiscardUnknown(m)
}
var xxx_messageInfo_Hook proto.InternalMessageInfo
func (m *Hook) GetUpload() *Upload {
if m != nil {
return m.Upload
}
return nil
}
func (m *Hook) GetHttpRequest() *HTTPRequest {
if m != nil {
return m.HttpRequest
}
return nil
}
func (m *Hook) GetName() string {
if m != nil {
return m.Name
}
return ""
}
// Request data to send hook
type SendRequest struct {
// The hook data
Hook *Hook `protobuf:"bytes,1,opt,name=hook,proto3" json:"hook,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *SendRequest) Reset() { *m = SendRequest{} }
func (m *SendRequest) String() string { return proto.CompactTextString(m) }
func (*SendRequest) ProtoMessage() {}
func (*SendRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_581082325ef044c1, []int{3}
}
func (m *SendRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_SendRequest.Unmarshal(m, b)
}
func (m *SendRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_SendRequest.Marshal(b, m, deterministic)
}
func (m *SendRequest) XXX_Merge(src proto.Message) {
xxx_messageInfo_SendRequest.Merge(m, src)
}
func (m *SendRequest) XXX_Size() int {
return xxx_messageInfo_SendRequest.Size(m)
}
func (m *SendRequest) XXX_DiscardUnknown() {
xxx_messageInfo_SendRequest.DiscardUnknown(m)
}
var xxx_messageInfo_SendRequest proto.InternalMessageInfo
func (m *SendRequest) GetHook() *Hook {
if m != nil {
return m.Hook
}
return nil
}
// Response that contains data for sended hook
type SendResponse struct {
// The response of the hook.
Response *any.Any `protobuf:"bytes,1,opt,name=response,proto3" json:"response,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *SendResponse) Reset() { *m = SendResponse{} }
func (m *SendResponse) String() string { return proto.CompactTextString(m) }
func (*SendResponse) ProtoMessage() {}
func (*SendResponse) Descriptor() ([]byte, []int) {
return fileDescriptor_581082325ef044c1, []int{4}
}
func (m *SendResponse) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_SendResponse.Unmarshal(m, b)
}
func (m *SendResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_SendResponse.Marshal(b, m, deterministic)
}
func (m *SendResponse) XXX_Merge(src proto.Message) {
xxx_messageInfo_SendResponse.Merge(m, src)
}
func (m *SendResponse) XXX_Size() int {
return xxx_messageInfo_SendResponse.Size(m)
}
func (m *SendResponse) XXX_DiscardUnknown() {
xxx_messageInfo_SendResponse.DiscardUnknown(m)
}
var xxx_messageInfo_SendResponse proto.InternalMessageInfo
func (m *SendResponse) GetResponse() *any.Any {
if m != nil {
return m.Response
}
return nil
}
func init() {
proto.RegisterType((*Upload)(nil), "v1.Upload")
proto.RegisterMapType((map[string]string)(nil), "v1.Upload.MetaDataEntry")
proto.RegisterMapType((map[string]string)(nil), "v1.Upload.StorageEntry")
proto.RegisterType((*HTTPRequest)(nil), "v1.HTTPRequest")
proto.RegisterType((*Hook)(nil), "v1.Hook")
proto.RegisterType((*SendRequest)(nil), "v1.SendRequest")
proto.RegisterType((*SendResponse)(nil), "v1.SendResponse")
}
func init() {
proto.RegisterFile("v1/hook.proto", fileDescriptor_581082325ef044c1)
}
var fileDescriptor_581082325ef044c1 = []byte{
// 477 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x52, 0x4d, 0x6f, 0xd3, 0x40,
0x10, 0x25, 0xb1, 0xeb, 0xd8, 0xe3, 0xb6, 0x54, 0xab, 0x0a, 0x96, 0xa8, 0x42, 0x96, 0x0f, 0xc8,
0x52, 0x25, 0x07, 0x07, 0x0e, 0x28, 0x5c, 0xa8, 0x54, 0x50, 0x39, 0x20, 0xaa, 0x4d, 0x11, 0xe7,
0x2d, 0xde, 0x24, 0x56, 0x1c, 0xaf, 0xbb, 0x5e, 0x5b, 0x0a, 0x3f, 0x8a, 0xdf, 0x88, 0xf6, 0xc3,
0x8d, 0xe9, 0x8d, 0x93, 0x67, 0xde, 0xbc, 0x79, 0xf3, 0x3c, 0x3b, 0x70, 0xd2, 0x65, 0xb3, 0x0d,
0xe7, 0xdb, 0xb4, 0x16, 0x5c, 0x72, 0x34, 0xee, 0xb2, 0xe9, 0xab, 0x35, 0xe7, 0xeb, 0x92, 0xcd,
0x34, 0x72, 0xdf, 0xae, 0x66, 0xb4, 0xda, 0x9b, 0x72, 0xfc, 0xc7, 0x01, 0xef, 0x47, 0x5d, 0x72,
0x9a, 0xa3, 0x53, 0x18, 0x17, 0x39, 0x1e, 0x45, 0xa3, 0x24, 0x20, 0xe3, 0x22, 0x47, 0x08, 0xdc,
0x65, 0xf1, 0x9b, 0xe1, 0x71, 0x34, 0x4a, 0x1c, 0xa2, 0x63, 0xf4, 0x06, 0x4e, 0xd5, 0xf7, 0x6b,
0x73, 0xcd, 0x56, 0x4c, 0x08, 0x96, 0x63, 0x27, 0x1a, 0x25, 0x3e, 0x79, 0x82, 0xa2, 0x17, 0xe0,
0x7d, 0x5f, 0xad, 0x1a, 0x26, 0xb1, 0xab, 0xbb, 0x6d, 0x86, 0xde, 0x83, 0xbf, 0x63, 0x92, 0x5e,
0x53, 0x49, 0xf1, 0x51, 0xe4, 0x24, 0xe1, 0x1c, 0xa7, 0x5d, 0x96, 0x1a, 0x07, 0xe9, 0x37, 0x5b,
0xfa, 0x5c, 0x49, 0xb1, 0x27, 0x8f, 0x4c, 0x74, 0x01, 0x41, 0xd1, 0xdc, 0x52, 0x21, 0x0b, 0x5a,
0x62, 0x4f, 0x0f, 0x3c, 0x00, 0x08, 0xc3, 0xa4, 0x68, 0xbe, 0x14, 0x15, 0x2d, 0xf1, 0x44, 0xd7,
0xfa, 0x54, 0xb9, 0xad, 0x0d, 0xc9, 0x0c, 0x68, 0xb0, 0x1f, 0x39, 0x49, 0x40, 0x9e, 0xa0, 0x28,
0x83, 0x49, 0x23, 0xb9, 0xa0, 0x6b, 0x86, 0x03, 0x6d, 0xea, 0xe5, 0xc0, 0xd4, 0xd2, 0x54, 0x8c,
0xa7, 0x9e, 0x37, 0xfd, 0x08, 0x27, 0xff, 0xb8, 0x45, 0x67, 0xe0, 0x6c, 0xd9, 0xde, 0xae, 0x4f,
0x85, 0xe8, 0x1c, 0x8e, 0x3a, 0x5a, 0xb6, 0x66, 0x81, 0x01, 0x31, 0xc9, 0x62, 0xfc, 0x61, 0x34,
0x5d, 0xc0, 0xf1, 0x50, 0xf5, 0x7f, 0x7a, 0xe3, 0x9f, 0x10, 0xde, 0xdc, 0xdd, 0xdd, 0x12, 0xf6,
0xd0, 0xb2, 0x46, 0xaa, 0x45, 0xef, 0x98, 0xdc, 0xf0, 0xfe, 0xe1, 0x6c, 0xa6, 0x24, 0x5b, 0x51,
0xd8, 0x76, 0x15, 0xa2, 0xd7, 0x00, 0x82, 0xed, 0xb8, 0x64, 0x57, 0x79, 0x2e, 0xf4, 0xb3, 0x05,
0x64, 0x80, 0xc4, 0x0f, 0xe0, 0xde, 0x70, 0xbe, 0x45, 0x31, 0x78, 0xad, 0xfe, 0x73, 0xad, 0x18,
0xce, 0xe1, 0xb0, 0x0b, 0x62, 0x2b, 0x28, 0x83, 0x70, 0x23, 0x65, 0x6d, 0x4d, 0xe8, 0x29, 0xe1,
0xfc, 0xb9, 0x22, 0x0e, 0xbc, 0x91, 0x21, 0x47, 0x5d, 0x53, 0x45, 0x77, 0xcc, 0x0e, 0xd6, 0x71,
0x7c, 0x09, 0xe1, 0x92, 0x55, 0x79, 0x4f, 0xb9, 0x00, 0x57, 0x1d, 0xae, 0x9d, 0xeb, 0x6b, 0x39,
0xce, 0xb7, 0x44, 0xa3, 0xf1, 0x27, 0x38, 0x36, 0xe4, 0xa6, 0xe6, 0x55, 0xc3, 0xd0, 0x5b, 0xf0,
0x85, 0x8d, 0x6d, 0xc7, 0x79, 0x6a, 0xee, 0x3c, 0xed, 0xef, 0x3c, 0xbd, 0xaa, 0xf6, 0xe4, 0x91,
0x35, 0x5f, 0x40, 0xa8, 0xf4, 0x96, 0x4c, 0x74, 0xc5, 0x2f, 0x86, 0x2e, 0xc1, 0x55, 0x82, 0x48,
0xfb, 0x1e, 0xf8, 0x98, 0x9e, 0x1d, 0x00, 0xd3, 0x19, 0x3f, 0xbb, 0xf7, 0xb4, 0xe6, 0xbb, 0xbf,
0x01, 0x00, 0x00, 0xff, 0xff, 0x8f, 0xd4, 0x14, 0x0d, 0x5e, 0x03, 0x00, 0x00,
}
// Reference imports to suppress errors if they are not otherwise used.
var _ context.Context
var _ grpc.ClientConnInterface
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
const _ = grpc.SupportPackageIsVersion6
// HookServiceClient is the client API for HookService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
type HookServiceClient interface {
// Sends a hook
Send(ctx context.Context, in *SendRequest, opts ...grpc.CallOption) (*SendResponse, error)
}
type hookServiceClient struct {
cc grpc.ClientConnInterface
}
func NewHookServiceClient(cc grpc.ClientConnInterface) HookServiceClient {
return &hookServiceClient{cc}
}
func (c *hookServiceClient) Send(ctx context.Context, in *SendRequest, opts ...grpc.CallOption) (*SendResponse, error) {
out := new(SendResponse)
err := c.cc.Invoke(ctx, "/v1.HookService/Send", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// HookServiceServer is the server API for HookService service.
type HookServiceServer interface {
// Sends a hook
Send(context.Context, *SendRequest) (*SendResponse, error)
}
// UnimplementedHookServiceServer can be embedded to have forward compatible implementations.
type UnimplementedHookServiceServer struct {
}
func (*UnimplementedHookServiceServer) Send(ctx context.Context, req *SendRequest) (*SendResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Send not implemented")
}
func RegisterHookServiceServer(s *grpc.Server, srv HookServiceServer) {
s.RegisterService(&_HookService_serviceDesc, srv)
}
func _HookService_Send_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SendRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(HookServiceServer).Send(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/v1.HookService/Send",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(HookServiceServer).Send(ctx, req.(*SendRequest))
}
return interceptor(ctx, in, info, handler)
}
var _HookService_serviceDesc = grpc.ServiceDesc{
ServiceName: "v1.HookService",
HandlerType: (*HookServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "Send",
Handler: _HookService_Send_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "v1/hook.proto",
}

555
pkg/proto/v2/hook.pb.go Normal file
View File

@ -0,0 +1,555 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: v2/hook.proto
package v2
import (
context "context"
fmt "fmt"
proto "github.com/golang/protobuf/proto"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
math "math"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
// Hook's data
type HookRequest struct {
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"`
Event *Event `protobuf:"bytes,2,opt,name=event,proto3" json:"event,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *HookRequest) Reset() { *m = HookRequest{} }
func (m *HookRequest) String() string { return proto.CompactTextString(m) }
func (*HookRequest) ProtoMessage() {}
func (*HookRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_938ab51c60d4b622, []int{0}
}
func (m *HookRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_HookRequest.Unmarshal(m, b)
}
func (m *HookRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_HookRequest.Marshal(b, m, deterministic)
}
func (m *HookRequest) XXX_Merge(src proto.Message) {
xxx_messageInfo_HookRequest.Merge(m, src)
}
func (m *HookRequest) XXX_Size() int {
return xxx_messageInfo_HookRequest.Size(m)
}
func (m *HookRequest) XXX_DiscardUnknown() {
xxx_messageInfo_HookRequest.DiscardUnknown(m)
}
var xxx_messageInfo_HookRequest proto.InternalMessageInfo
func (m *HookRequest) GetType() string {
if m != nil {
return m.Type
}
return ""
}
func (m *HookRequest) GetEvent() *Event {
if m != nil {
return m.Event
}
return nil
}
type Event struct {
Upload *FileInfo `protobuf:"bytes,1,opt,name=upload,proto3" json:"upload,omitempty"`
HttpRequest *HTTPRequest `protobuf:"bytes,2,opt,name=httpRequest,proto3" json:"httpRequest,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *Event) Reset() { *m = Event{} }
func (m *Event) String() string { return proto.CompactTextString(m) }
func (*Event) ProtoMessage() {}
func (*Event) Descriptor() ([]byte, []int) {
return fileDescriptor_938ab51c60d4b622, []int{1}
}
func (m *Event) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_Event.Unmarshal(m, b)
}
func (m *Event) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_Event.Marshal(b, m, deterministic)
}
func (m *Event) XXX_Merge(src proto.Message) {
xxx_messageInfo_Event.Merge(m, src)
}
func (m *Event) XXX_Size() int {
return xxx_messageInfo_Event.Size(m)
}
func (m *Event) XXX_DiscardUnknown() {
xxx_messageInfo_Event.DiscardUnknown(m)
}
var xxx_messageInfo_Event proto.InternalMessageInfo
func (m *Event) GetUpload() *FileInfo {
if m != nil {
return m.Upload
}
return nil
}
func (m *Event) GetHttpRequest() *HTTPRequest {
if m != nil {
return m.HttpRequest
}
return nil
}
// TODO: Keep consistent naming capitalization
// Uploaded data
type FileInfo struct {
// Unique integer identifier of the uploaded file
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
// Total file size in bytes specified in the NewUpload call
Size int64 `protobuf:"varint,2,opt,name=size,proto3" json:"size,omitempty"`
// Indicates whether the total file size is deferred until later
SizeIsDeferred bool `protobuf:"varint,3,opt,name=sizeIsDeferred,proto3" json:"sizeIsDeferred,omitempty"`
// Offset in bytes (zero-based)
Offset int64 `protobuf:"varint,4,opt,name=offset,proto3" json:"offset,omitempty"`
MetaData map[string]string `protobuf:"bytes,5,rep,name=metaData,proto3" json:"metaData,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
// Indicates that this is a partial upload which will later be used to form
// a final upload by concatenation. Partial uploads should not be processed
// when they are finished since they are only incomplete chunks of files.
IsPartial bool `protobuf:"varint,6,opt,name=isPartial,proto3" json:"isPartial,omitempty"`
// Indicates that this is a final upload
IsFinal bool `protobuf:"varint,7,opt,name=isFinal,proto3" json:"isFinal,omitempty"`
// If the upload is a final one (see IsFinal) this will be a non-empty
// ordered slice containing the ids of the uploads of which the final upload
// will consist after concatenation.
PartialUploads []string `protobuf:"bytes,8,rep,name=partialUploads,proto3" json:"partialUploads,omitempty"`
// Storage contains information about where the data storage saves the upload,
// for example a file path. The available values vary depending on what data
// store is used. This map may also be nil.
Storage map[string]string `protobuf:"bytes,9,rep,name=storage,proto3" json:"storage,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *FileInfo) Reset() { *m = FileInfo{} }
func (m *FileInfo) String() string { return proto.CompactTextString(m) }
func (*FileInfo) ProtoMessage() {}
func (*FileInfo) Descriptor() ([]byte, []int) {
return fileDescriptor_938ab51c60d4b622, []int{2}
}
func (m *FileInfo) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_FileInfo.Unmarshal(m, b)
}
func (m *FileInfo) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_FileInfo.Marshal(b, m, deterministic)
}
func (m *FileInfo) XXX_Merge(src proto.Message) {
xxx_messageInfo_FileInfo.Merge(m, src)
}
func (m *FileInfo) XXX_Size() int {
return xxx_messageInfo_FileInfo.Size(m)
}
func (m *FileInfo) XXX_DiscardUnknown() {
xxx_messageInfo_FileInfo.DiscardUnknown(m)
}
var xxx_messageInfo_FileInfo proto.InternalMessageInfo
func (m *FileInfo) GetId() string {
if m != nil {
return m.Id
}
return ""
}
func (m *FileInfo) GetSize() int64 {
if m != nil {
return m.Size
}
return 0
}
func (m *FileInfo) GetSizeIsDeferred() bool {
if m != nil {
return m.SizeIsDeferred
}
return false
}
func (m *FileInfo) GetOffset() int64 {
if m != nil {
return m.Offset
}
return 0
}
func (m *FileInfo) GetMetaData() map[string]string {
if m != nil {
return m.MetaData
}
return nil
}
func (m *FileInfo) GetIsPartial() bool {
if m != nil {
return m.IsPartial
}
return false
}
func (m *FileInfo) GetIsFinal() bool {
if m != nil {
return m.IsFinal
}
return false
}
func (m *FileInfo) GetPartialUploads() []string {
if m != nil {
return m.PartialUploads
}
return nil
}
func (m *FileInfo) GetStorage() map[string]string {
if m != nil {
return m.Storage
}
return nil
}
type HTTPRequest struct {
// Method is the HTTP method, e.g. POST or PATCH
Method string `protobuf:"bytes,1,opt,name=method,proto3" json:"method,omitempty"`
// URI is the full HTTP request URI, e.g. /files/fooo
Uri string `protobuf:"bytes,2,opt,name=uri,proto3" json:"uri,omitempty"`
// RemoteAddr contains the network address that sent the request
RemoteAddr string `protobuf:"bytes,3,opt,name=remoteAddr,proto3" json:"remoteAddr,omitempty"`
Header map[string]string `protobuf:"bytes,4,rep,name=header,proto3" json:"header,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *HTTPRequest) Reset() { *m = HTTPRequest{} }
func (m *HTTPRequest) String() string { return proto.CompactTextString(m) }
func (*HTTPRequest) ProtoMessage() {}
func (*HTTPRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_938ab51c60d4b622, []int{3}
}
func (m *HTTPRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_HTTPRequest.Unmarshal(m, b)
}
func (m *HTTPRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_HTTPRequest.Marshal(b, m, deterministic)
}
func (m *HTTPRequest) XXX_Merge(src proto.Message) {
xxx_messageInfo_HTTPRequest.Merge(m, src)
}
func (m *HTTPRequest) XXX_Size() int {
return xxx_messageInfo_HTTPRequest.Size(m)
}
func (m *HTTPRequest) XXX_DiscardUnknown() {
xxx_messageInfo_HTTPRequest.DiscardUnknown(m)
}
var xxx_messageInfo_HTTPRequest proto.InternalMessageInfo
func (m *HTTPRequest) GetMethod() string {
if m != nil {
return m.Method
}
return ""
}
func (m *HTTPRequest) GetUri() string {
if m != nil {
return m.Uri
}
return ""
}
func (m *HTTPRequest) GetRemoteAddr() string {
if m != nil {
return m.RemoteAddr
}
return ""
}
func (m *HTTPRequest) GetHeader() map[string]string {
if m != nil {
return m.Header
}
return nil
}
type HookResponse struct {
HttpResponse *HTTPResponse `protobuf:"bytes,1,opt,name=httpResponse,proto3" json:"httpResponse,omitempty"`
RejectUpload bool `protobuf:"varint,2,opt,name=rejectUpload,proto3" json:"rejectUpload,omitempty"`
StopUpload bool `protobuf:"varint,3,opt,name=stopUpload,proto3" json:"stopUpload,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *HookResponse) Reset() { *m = HookResponse{} }
func (m *HookResponse) String() string { return proto.CompactTextString(m) }
func (*HookResponse) ProtoMessage() {}
func (*HookResponse) Descriptor() ([]byte, []int) {
return fileDescriptor_938ab51c60d4b622, []int{4}
}
func (m *HookResponse) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_HookResponse.Unmarshal(m, b)
}
func (m *HookResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_HookResponse.Marshal(b, m, deterministic)
}
func (m *HookResponse) XXX_Merge(src proto.Message) {
xxx_messageInfo_HookResponse.Merge(m, src)
}
func (m *HookResponse) XXX_Size() int {
return xxx_messageInfo_HookResponse.Size(m)
}
func (m *HookResponse) XXX_DiscardUnknown() {
xxx_messageInfo_HookResponse.DiscardUnknown(m)
}
var xxx_messageInfo_HookResponse proto.InternalMessageInfo
func (m *HookResponse) GetHttpResponse() *HTTPResponse {
if m != nil {
return m.HttpResponse
}
return nil
}
func (m *HookResponse) GetRejectUpload() bool {
if m != nil {
return m.RejectUpload
}
return false
}
func (m *HookResponse) GetStopUpload() bool {
if m != nil {
return m.StopUpload
}
return false
}
type HTTPResponse struct {
StatusCode int64 `protobuf:"varint,1,opt,name=statusCode,proto3" json:"statusCode,omitempty"`
Headers map[string]string `protobuf:"bytes,2,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
Body string `protobuf:"bytes,3,opt,name=body,proto3" json:"body,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *HTTPResponse) Reset() { *m = HTTPResponse{} }
func (m *HTTPResponse) String() string { return proto.CompactTextString(m) }
func (*HTTPResponse) ProtoMessage() {}
func (*HTTPResponse) Descriptor() ([]byte, []int) {
return fileDescriptor_938ab51c60d4b622, []int{5}
}
func (m *HTTPResponse) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_HTTPResponse.Unmarshal(m, b)
}
func (m *HTTPResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_HTTPResponse.Marshal(b, m, deterministic)
}
func (m *HTTPResponse) XXX_Merge(src proto.Message) {
xxx_messageInfo_HTTPResponse.Merge(m, src)
}
func (m *HTTPResponse) XXX_Size() int {
return xxx_messageInfo_HTTPResponse.Size(m)
}
func (m *HTTPResponse) XXX_DiscardUnknown() {
xxx_messageInfo_HTTPResponse.DiscardUnknown(m)
}
var xxx_messageInfo_HTTPResponse proto.InternalMessageInfo
func (m *HTTPResponse) GetStatusCode() int64 {
if m != nil {
return m.StatusCode
}
return 0
}
func (m *HTTPResponse) GetHeaders() map[string]string {
if m != nil {
return m.Headers
}
return nil
}
func (m *HTTPResponse) GetBody() string {
if m != nil {
return m.Body
}
return ""
}
func init() {
proto.RegisterType((*HookRequest)(nil), "v2.HookRequest")
proto.RegisterType((*Event)(nil), "v2.Event")
proto.RegisterType((*FileInfo)(nil), "v2.FileInfo")
proto.RegisterMapType((map[string]string)(nil), "v2.FileInfo.MetaDataEntry")
proto.RegisterMapType((map[string]string)(nil), "v2.FileInfo.StorageEntry")
proto.RegisterType((*HTTPRequest)(nil), "v2.HTTPRequest")
proto.RegisterMapType((map[string]string)(nil), "v2.HTTPRequest.HeaderEntry")
proto.RegisterType((*HookResponse)(nil), "v2.HookResponse")
proto.RegisterType((*HTTPResponse)(nil), "v2.HTTPResponse")
proto.RegisterMapType((map[string]string)(nil), "v2.HTTPResponse.HeadersEntry")
}
func init() {
proto.RegisterFile("v2/hook.proto", fileDescriptor_938ab51c60d4b622)
}
var fileDescriptor_938ab51c60d4b622 = []byte{
// 578 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x54, 0xdb, 0x6e, 0xd3, 0x40,
0x10, 0xc5, 0x71, 0x73, 0xf1, 0x38, 0x2d, 0xd5, 0x0a, 0xa1, 0x25, 0xdc, 0x22, 0x0b, 0xa1, 0x3c,
0x05, 0xd5, 0x45, 0x5c, 0xca, 0x0b, 0x97, 0xb6, 0x4a, 0x1f, 0x90, 0xaa, 0xa5, 0xbc, 0xb3, 0xc5,
0x13, 0x62, 0xe2, 0x7a, 0xcd, 0xee, 0xc6, 0x52, 0xf8, 0x02, 0x3e, 0x08, 0x89, 0x4f, 0xe0, 0xb7,
0xd0, 0x5e, 0x42, 0x9c, 0xbc, 0xe5, 0xc9, 0x3b, 0x67, 0xce, 0xcc, 0x9e, 0x3d, 0x3b, 0x5e, 0xd8,
0xaf, 0xd3, 0x67, 0x33, 0x21, 0xe6, 0xe3, 0x4a, 0x0a, 0x2d, 0x48, 0xab, 0x4e, 0x93, 0xf7, 0x10,
0x4f, 0x84, 0x98, 0x33, 0xfc, 0xb1, 0x40, 0xa5, 0x09, 0x81, 0x3d, 0xbd, 0xac, 0x90, 0x06, 0xc3,
0x60, 0x14, 0x31, 0xbb, 0x26, 0x8f, 0xa1, 0x8d, 0x35, 0x96, 0x9a, 0xb6, 0x86, 0xc1, 0x28, 0x4e,
0xa3, 0x71, 0x9d, 0x8e, 0xcf, 0x0c, 0xc0, 0x1c, 0x9e, 0x7c, 0x81, 0xb6, 0x8d, 0xc9, 0x13, 0xe8,
0x2c, 0xaa, 0x42, 0xf0, 0xcc, 0xd6, 0xc7, 0x69, 0xdf, 0x50, 0xcf, 0xf3, 0x02, 0x2f, 0xca, 0xa9,
0x60, 0x3e, 0x47, 0x8e, 0x20, 0x9e, 0x69, 0x5d, 0xf9, 0x2d, 0x7d, 0xd7, 0xdb, 0x86, 0x3a, 0xb9,
0xba, 0xba, 0xf4, 0x30, 0x6b, 0x72, 0x92, 0xdf, 0x21, 0xf4, 0x56, 0x7d, 0xc8, 0x01, 0xb4, 0xf2,
0xcc, 0x2b, 0x6c, 0xe5, 0x99, 0xd1, 0xac, 0xf2, 0x9f, 0x68, 0x1b, 0x85, 0xcc, 0xae, 0xc9, 0x53,
0x38, 0x30, 0xdf, 0x0b, 0x75, 0x8a, 0x53, 0x94, 0x12, 0x33, 0x1a, 0x0e, 0x83, 0x51, 0x8f, 0x6d,
0xa1, 0xe4, 0x2e, 0x74, 0xc4, 0x74, 0xaa, 0x50, 0xd3, 0x3d, 0x5b, 0xed, 0x23, 0xf2, 0x02, 0x7a,
0x37, 0xa8, 0xf9, 0x29, 0xd7, 0x9c, 0xb6, 0x87, 0xe1, 0x28, 0x4e, 0x07, 0xcd, 0xb3, 0x8c, 0x3f,
0xfa, 0xe4, 0x59, 0xa9, 0xe5, 0x92, 0xfd, 0xe7, 0x92, 0x07, 0x10, 0xe5, 0xea, 0x92, 0x4b, 0x9d,
0xf3, 0x82, 0x76, 0xec, 0x96, 0x6b, 0x80, 0x50, 0xe8, 0xe6, 0xea, 0x3c, 0x2f, 0x79, 0x41, 0xbb,
0x36, 0xb7, 0x0a, 0x8d, 0xde, 0xca, 0x91, 0x3e, 0x5b, 0x93, 0x14, 0xed, 0x0d, 0xc3, 0x51, 0xc4,
0xb6, 0x50, 0x72, 0x0c, 0x5d, 0xa5, 0x85, 0xe4, 0xdf, 0x90, 0x46, 0x56, 0xd6, 0xbd, 0x0d, 0x59,
0x9f, 0x5c, 0xce, 0xa9, 0x5a, 0x31, 0x07, 0x6f, 0x60, 0x7f, 0x43, 0x2f, 0x39, 0x84, 0x70, 0x8e,
0x4b, 0x6f, 0xa1, 0x59, 0x92, 0x3b, 0xd0, 0xae, 0x79, 0xb1, 0x70, 0x26, 0x46, 0xcc, 0x05, 0x27,
0xad, 0x57, 0xc1, 0xe0, 0x04, 0xfa, 0xcd, 0xae, 0xbb, 0xd4, 0x26, 0x7f, 0x03, 0x88, 0x1b, 0x77,
0x6a, 0xdc, 0xbe, 0x41, 0x3d, 0x13, 0xab, 0xdb, 0xf3, 0x91, 0xe9, 0xb9, 0x90, 0xb9, 0xaf, 0x37,
0x4b, 0xf2, 0x08, 0x40, 0xe2, 0x8d, 0xd0, 0xf8, 0x2e, 0xcb, 0xa4, 0xbd, 0xbb, 0x88, 0x35, 0x10,
0x72, 0x0c, 0x9d, 0x19, 0xf2, 0x0c, 0x25, 0xdd, 0xb3, 0x36, 0xdc, 0xdf, 0x1a, 0x9f, 0xf1, 0xc4,
0x66, 0x9d, 0x11, 0x9e, 0x3a, 0x78, 0x0d, 0x71, 0x03, 0xde, 0xe9, 0x24, 0xbf, 0x02, 0xe8, 0xbb,
0xff, 0x44, 0x55, 0xa2, 0x54, 0x48, 0x9e, 0x43, 0xdf, 0x0d, 0xa8, 0x8b, 0xfd, 0xc0, 0x1f, 0xae,
0x65, 0x38, 0x9c, 0x6d, 0xb0, 0x48, 0x02, 0x7d, 0x89, 0xdf, 0xf1, 0xab, 0x76, 0xf7, 0x69, 0xf7,
0xe9, 0xb1, 0x0d, 0xcc, 0x1c, 0x5d, 0x69, 0x51, 0x79, 0x86, 0x1b, 0xdb, 0x06, 0x92, 0xfc, 0x31,
0x52, 0x1a, 0x5b, 0xb8, 0x02, 0xae, 0x17, 0xea, 0x83, 0xc8, 0x9c, 0x90, 0x90, 0x35, 0x10, 0xf2,
0x12, 0xba, 0xce, 0x00, 0x45, 0x5b, 0xd6, 0xac, 0x87, 0xdb, 0x2a, 0xbd, 0x5b, 0xca, 0xcf, 0x8d,
0x67, 0x9b, 0x1f, 0xeb, 0x5a, 0x64, 0x4b, 0x6f, 0xbf, 0x5d, 0x9b, 0x71, 0x68, 0x92, 0x77, 0x31,
0x31, 0x7d, 0xeb, 0xde, 0x9a, 0x09, 0x2f, 0xb3, 0x02, 0x25, 0x39, 0x02, 0xb8, 0x28, 0x6b, 0x31,
0x47, 0x03, 0x12, 0xf7, 0x00, 0xac, 0x9f, 0xa2, 0xc1, 0xe1, 0x1a, 0x70, 0x2a, 0x93, 0x5b, 0xd7,
0x1d, 0xfb, 0x70, 0x1d, 0xff, 0x0b, 0x00, 0x00, 0xff, 0xff, 0xdc, 0x73, 0x61, 0x1c, 0xc9, 0x04,
0x00, 0x00,
}
// Reference imports to suppress errors if they are not otherwise used.
var _ context.Context
var _ grpc.ClientConnInterface
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
const _ = grpc.SupportPackageIsVersion6
// HookHandlerClient is the client API for HookHandler service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
type HookHandlerClient interface {
// Sends a hook
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.
type HookHandlerServer interface {
// Sends a hook
InvokeHook(context.Context, *HookRequest) (*HookResponse, error)
}
// UnimplementedHookHandlerServer can be embedded to have forward compatible implementations.
type UnimplementedHookHandlerServer struct {
}
func (*UnimplementedHookHandlerServer) InvokeHook(ctx context.Context, req *HookRequest) (*HookResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method InvokeHook not implemented")
}
func RegisterHookHandlerServer(s *grpc.Server, 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)
}
var _HookHandler_serviceDesc = grpc.ServiceDesc{
ServiceName: "v2.HookHandler",
HandlerType: (*HookHandlerServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "InvokeHook",
Handler: _HookHandler_InvokeHook_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "v2/hook.proto",
}

View File

@ -717,7 +717,7 @@ func (upload s3Upload) GetReader(ctx context.Context) (io.Reader, error) {
}) })
if err == nil { if err == nil {
// The multipart upload still exists, which means we cannot download it yet // The multipart upload still exists, which means we cannot download it yet
return nil, handler.NewHTTPError("ERR_INCOMPLETE_UPLOAD", "cannot stream non-finished upload", http.StatusBadRequest) return nil, handler.NewError("ERR_INCOMPLETE_UPLOAD", "cannot stream non-finished upload", http.StatusBadRequest)
} }
if isAwsError(err, "NoSuchUpload") { if isAwsError(err, "NoSuchUpload") {

View File

@ -576,7 +576,7 @@ func TestGetReaderNotFinished(t *testing.T) {
content, err := upload.GetReader(context.Background()) content, err := upload.GetReader(context.Background())
assert.Nil(content) assert.Nil(content)
assert.Equal("cannot stream non-finished upload", err.Error()) assert.Equal("ERR_INCOMPLETE_UPLOAD: cannot stream non-finished upload", err.Error())
} }
func TestDeclareLength(t *testing.T) { func TestDeclareLength(t *testing.T) {