Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .DS_Store
Binary file not shown.
37 changes: 32 additions & 5 deletions file-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,17 +52,44 @@ paths:

## Server side

The handler receives a `io.ReadCloser` as the file to consume.
For `x-go-server-streaming: true`, generated binding only constructs a
`*runtime.MultipartFormStream` and passes ownership to the handler. It does not
consume parts or try to populate generated file and form fields.

Under the hood, the runtime builds this with a `*runtime.File`, which provides access to some header information, such as:
The handler traverses the multipart body sequentially:

```go
if namedFile, ok := params.File.(*runtime.File); ok {
log.Printf("received file name: %s", namedFile.Header.Filename)
log.Printf("received file size: %d", namedFile.Header.Size)
for {
file, err := params.MultipartForm.NextFile()
if errors.Is(err, io.EOF) {
break
}
// Validate file.FieldName and consume file here.
}
```

Each `runtime.StreamedFile` reads directly from the HTTP request body. The
filename and MIME headers are available before the payload is consumed, but the
complete file size is not known in advance.

## Server-side streaming

The server binding is generated from `x-go-server-streaming: true`. The
generated binder creates a `*runtime.MultipartFormStream` without reading
multipart parts ahead of the handler. Required fields, accepted file field
names, multiplicity and other application-specific rules are validated by the
handler while traversing the stream.

`MultipartFormStream.Fields()` and `MultipartFormStream.Files()` return
snapshots of ordinary fields and file metadata discovered so far. They do not
read ahead: trailing fields become visible only after the active file is
consumed or closed and the stream advances.

The handler owns the multipart stream and must either:

- call `Drain()` to process all remaining parts and close the request body; or
- call `Close()` to abort multipart processing.

## Client side

The local file is handled as a `runtime.NamedReadCloser` (that is, a `io.ReadCloser` plus the `Name() string` method).
Expand Down
86 changes: 64 additions & 22 deletions file-server/restapi/configure_file_upload.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ func configureAPI(api *operations.FileUploadAPI) http.Handler {

api.JSONProducer = runtime.JSONProducer()

// You may change here the memory limit for this multipart form parser. Below is the default (32 MB).
// uploads.UploadFileMaxParseMemory = 32 << 20
// You may change here the maximum body size for this streaming multipart form. Below is the default (32 MB).
// uploads.UploadFileMaxBodySize = 32 << 20

uploadFolder, err := os.MkdirTemp(".", "upload")
if err != nil {
Expand All @@ -57,34 +57,55 @@ func configureAPI(api *operations.FileUploadAPI) http.Handler {

// snippet:upload-handler
api.UploadsUploadFileHandler = uploads.UploadFileHandlerFunc(func(params uploads.UploadFileParams) middleware.Responder {
if params.File == nil {
return middleware.Error(http.StatusNotFound, stderrors.New("no file provided"))
if params.MultipartForm == nil {
return middleware.Error(http.StatusInternalServerError, stderrors.New("multipart stream is not initialized"))
}
defer func() {
_ = params.File.Close()
_ = params.MultipartForm.Close()
}()

if namedFile, ok := params.File.(*runtime.File); ok {
log.Printf("received file name: %s", namedFile.Header.Filename)
log.Printf("received file size: %d", namedFile.Header.Size)
uploadedFiles := 0
for {
file, err := params.MultipartForm.NextFile()
if stderrors.Is(err, io.EOF) {
break
}
if err != nil {
return uploadError(err)
}

if file.FieldName != "file" {
if err := file.Close(); err != nil {
return uploadError(err)
}

continue
}

log.Printf("received file name: %s", file.Filename)
log.Printf("received content type: %s", file.Header.Get(runtime.HeaderContentType))

filename := path.Join(uploadFolder, fmt.Sprintf("uploaded_file_%d.dat", uploadCounter))
uploadCounter++
n, err := saveStreamedFile(filename, file)
if err != nil {
return uploadError(err)
}
uploadedFiles++

log.Printf("copied bytes %d", n)
log.Printf("file uploaded copied as %s", filename)
}

// uploads file and save it locally
filename := path.Join(uploadFolder, fmt.Sprintf("uploaded_file_%d.dat", uploadCounter))
uploadCounter++
f, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600)
if err != nil {
return middleware.Error(http.StatusInternalServerError, stderrors.New("could not create file on server"))
if uploadedFiles == 0 {
return middleware.Error(http.StatusBadRequest, stderrors.New("no file provided"))
}

n, err := io.Copy(f, params.File)
if err != nil {
return middleware.Error(http.StatusInternalServerError, stderrors.New("could not upload file on server"))
if err := params.MultipartForm.Drain(); err != nil {
return uploadError(err)
}

log.Printf("copied bytes %d", n)

log.Printf("file uploaded copied as %s", filename)
log.Printf("discovered multipart fields: %v", params.MultipartForm.Fields())
log.Printf("discovered multipart files: %d", len(params.MultipartForm.Files()))

return uploads.NewUploadFileOK()
})
Expand Down Expand Up @@ -122,5 +143,26 @@ func setupMiddlewares(handler http.Handler) http.Handler {
// The middleware configuration happens before anything, this middleware also applies to serving the swagger.json document.
// So this is a good place to plug in a panic handling middleware, logging and metrics.
func setupGlobalMiddleware(handler http.Handler) http.Handler {
return handler
return http.MaxBytesHandler(handler, uploads.UploadFileMaxBodySize)
}

func saveStreamedFile(filename string, file io.Reader) (int64, error) {
f, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600)
if err != nil {
return 0, fmt.Errorf("create upload file: %w", err)
}

n, copyErr := io.Copy(f, file)
closeErr := f.Close()

return n, stderrors.Join(copyErr, closeErr)
}

func uploadError(err error) middleware.Responder {
var maxBytesErr *http.MaxBytesError
if stderrors.As(err, &maxBytesErr) {
return middleware.Error(http.StatusRequestEntityTooLarge, err)
}

return middleware.Error(http.StatusInternalServerError, stderrors.New("could not upload file on server"))
}
2 changes: 2 additions & 0 deletions file-server/restapi/embedded_spec.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 13 additions & 32 deletions file-server/restapi/operations/uploads/upload_file_parameters.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading