-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Introduce @requestBody.file and @oas.response.file decorators #4882
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
92292aa
feat(openapi-v3): add sugar decorators for file requestBody/response
raymondfeng 0fe0c27
feat(docs): add docs for @requestBody.file and @oas.respone.file
raymondfeng 8ed92b0
feat(example-file-upload-download): use @requestBody.file and @oas.re…
raymondfeng a6088a6
feat(docs): add file upload/download to usage scenarios
raymondfeng File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,301 @@ | ||
--- | ||
lang: en | ||
title: 'Upload and download files' | ||
keywords: LoopBack 4.0, LoopBack-Next | ||
sidebar: lb4_sidebar | ||
permalink: /doc/en/lb4/File-upload-download.html | ||
--- | ||
|
||
## File upload and download | ||
|
||
It's a common requirement for API applications to support file upload and | ||
download. This page describes how to expose REST APIs for uploading and | ||
downloading files using LoopBack 4. It also illustrates how to build a simple | ||
Web UI to interact with such APIs. | ||
|
||
A fully-functional example is available at | ||
[@loopback/example-file-upload-download](https://github.com/strongloop/loopback-next/tree/master/examples/file-upload-download). | ||
We use code snippets from the example to walk through the key artifacts. | ||
|
||
### File upload | ||
|
||
A few steps are involved to create an endpoint for file upload. | ||
|
||
1. Create a controller class such as | ||
[`FileUploadController`](https://github.com/strongloop/loopback-next/blob/master/examples/file-upload-download/src/controllers/file-upload.controller.ts) | ||
|
||
```ts | ||
import {inject} from '@loopback/context'; | ||
import { | ||
post, | ||
Request, | ||
requestBody, | ||
Response, | ||
RestBindings, | ||
} from '@loopback/rest'; | ||
import {FILE_UPLOAD_SERVICE} from '../keys'; | ||
import {FileUploadHandler} from '../types'; | ||
/** | ||
* A controller to handle file uploads using multipart/form-data media type | ||
*/ | ||
export class FileUploadController { | ||
/** | ||
* Constructor | ||
* @param handler - Inject an express request handler to deal with the request | ||
*/ | ||
constructor( | ||
@inject(FILE_UPLOAD_SERVICE) private handler: FileUploadHandler, | ||
) {} | ||
} | ||
``` | ||
|
||
In the example, we inject an instance of `FileUploadService` backed by | ||
[multer](https://github.com/expressjs/multer) to process the incoming http | ||
request. The | ||
[`FileUploadService`](https://github.com/strongloop/loopback-next/blob/master/examples/file-upload-download/src/services/file-upload.service.ts) | ||
is configurable to support various storage engines. | ||
|
||
2. Add a method for file upload | ||
|
||
```ts | ||
/** | ||
* A controller to handle file uploads using multipart/form-data media type | ||
*/ | ||
export class FileUploadController { | ||
@post('/files', { | ||
responses: { | ||
200: { | ||
content: { | ||
'application/json': { | ||
schema: { | ||
type: 'object', | ||
}, | ||
}, | ||
}, | ||
description: 'Files and fields', | ||
}, | ||
}, | ||
}) | ||
async fileUpload( | ||
@requestBody.file() | ||
request: Request, | ||
@inject(RestBindings.Http.RESPONSE) response: Response, | ||
): Promise<object> { | ||
return new Promise<object>((resolve, reject) => { | ||
this.handler(request, response, err => { | ||
if (err) reject(err); | ||
else { | ||
resolve(FileUploadController.getFilesAndFields(request)); | ||
} | ||
}); | ||
}); | ||
} | ||
} | ||
``` | ||
|
||
The `@post` decoration exposes the method over `POST /files` endpoint to accept | ||
file upload. We also apply `@requestBody.file()` to mark the request body to be | ||
files being uploaded using `multipart/form-data` content type. The injected | ||
`request` and `response` objects are passed into the `multer` handler to process | ||
the stream, saving to `.sandbox` directory in our example. | ||
|
||
See more details about `@requestBody.file` in | ||
[OpenAPI decorators](decorators/Decorators_openapi.md#requestbodyfile). | ||
|
||
### File download | ||
|
||
To download files from the backend, please follow the following steps. | ||
|
||
1. Create a controller class such as | ||
[`FileDownloadController`](https://github.com/strongloop/loopback-next/blob/master/examples/file-upload-download/src/controllers/file-download.controller.ts) | ||
|
||
```ts | ||
import {inject} from '@loopback/context'; | ||
import { | ||
get, | ||
HttpErrors, | ||
oas, | ||
param, | ||
Response, | ||
RestBindings, | ||
} from '@loopback/rest'; | ||
import fs from 'fs'; | ||
import path from 'path'; | ||
import {promisify} from 'util'; | ||
|
||
const readdir = promisify(fs.readdir); | ||
|
||
const SANDBOX = path.resolve(__dirname, '../../.sandbox'); | ||
|
||
/** | ||
* A controller to handle file downloads using multipart/form-data media type | ||
*/ | ||
export class FileDownloadController {} | ||
``` | ||
|
||
2. (optional) Add a method to list available files | ||
|
||
```ts | ||
export class FileDownloadController { | ||
@get('/files', { | ||
responses: { | ||
200: { | ||
content: { | ||
// string[] | ||
'application/json': { | ||
schema: { | ||
type: 'array', | ||
items: { | ||
type: 'string', | ||
}, | ||
}, | ||
}, | ||
}, | ||
description: 'A list of files', | ||
}, | ||
}, | ||
}) | ||
async listFiles() { | ||
const files = await readdir(SANDBOX); | ||
return files; | ||
} | ||
``` | ||
|
||
The `@get` decorator exposes `GET /files` to list available file. Our example | ||
implementation simply returns an array of file names under the `.sandbox` | ||
directory. | ||
|
||
3. Add a method to download a file by name | ||
|
||
```ts | ||
export class FileDownloadController { | ||
@get('/files/{filename}') | ||
@oas.response.file() | ||
downloadFile( | ||
@param.path.string('filename') fileName: string, | ||
@inject(RestBindings.Http.RESPONSE) response: Response, | ||
) { | ||
const file = validateFileName(fileName); | ||
response.download(file, fileName); | ||
return response; | ||
} | ||
} | ||
|
||
/** | ||
* Validate file names to prevent them goes beyond the designated directory | ||
* @param fileName - File name | ||
*/ | ||
function validateFileName(fileName: string) { | ||
const resolved = path.resolve(SANDBOX, fileName); | ||
if (resolved.startsWith(SANDBOX)) return resolved; | ||
// The resolved file is outside sandbox | ||
throw new HttpErrors.BadRequest(`Invalid file name: ${fileName}`); | ||
} | ||
``` | ||
|
||
The `@get` decorator exposes `GET /files/{filename}` for file download. We use | ||
`response.download` from `Express` to send the file. | ||
|
||
The decoration of `@oas.response.file()` sets the OpenAPI response object for | ||
file download. See more details about `@oas.response.file` in | ||
[OpenAPI decorators](decorators/Decorators_openapi.md#using-oasresponsefile). | ||
|
||
{% include note.html content=" | ||
The `downloadFile` returns `response` as-is to instruct LoopBack to skip the | ||
response serialization step as `response.download` manipulates the `response` | ||
stream directly. | ||
" %} | ||
|
||
{% include warning.html content=" | ||
The `fileName` argument is from user input. We have to validate the value to | ||
prevent the request to access files outside the `.sandbox` directory. The | ||
`validateFileName` method resolves the file by name and rejects the request if | ||
the file is outside the sandbox. | ||
" %} | ||
|
||
### Build a simple UI | ||
|
||
The example application comes with a | ||
[simple HTML page](https://github.com/strongloop/loopback-next/blob/master/examples/file-upload-download/public/index.html). | ||
|
||
The page contains the following JavaScript functions: | ||
|
||
```js | ||
/** | ||
* Submit the upload form | ||
*/ | ||
function setupUploadForm() { | ||
const formElem = document.getElementById('uploadForm'); | ||
formElem.onsubmit = async e => { | ||
e.preventDefault(); | ||
const res = await fetch('/files', { | ||
method: 'POST', | ||
body: new FormData(formElem), | ||
}); | ||
const body = await res.json(); | ||
console.log('Response from upload', body); | ||
await fetchFiles(); | ||
}; | ||
} | ||
/** | ||
* List uploaded files | ||
*/ | ||
async function fetchFiles() { | ||
const res = await fetch('/files'); | ||
const files = await res.json(); | ||
console.log('Response from list', files); | ||
const list = files.map(f => `<li><a href="/files/${f}">${f}</a></li>\n`); | ||
document.getElementById('fileList').innerHTML = list.join(''); | ||
} | ||
async function init() { | ||
setupUploadForm(); | ||
await fetchFiles(); | ||
} | ||
``` | ||
|
||
The page has two key divisions: | ||
|
||
- A form for file selection and upload | ||
- A list of files with URL links to be downloaded | ||
|
||
```html | ||
<body onload="init();"> | ||
<div class="info"> | ||
<h1>File upload and download</h1> | ||
|
||
<div id="upload"> | ||
<h3>Upload files</h3> | ||
<form id="uploadForm"> | ||
<label for="files">Select files:</label> | ||
<input type="file" id="files" name="files" multiple /><br /><br /> | ||
<label for="note">Note:</label> | ||
<input | ||
type="text" | ||
name="note" | ||
id="note" | ||
placeholder="Note about the files" | ||
/> | ||
<br /><br /> | ||
<input type="submit" /> | ||
</form> | ||
</div> | ||
|
||
<div id="download"> | ||
<h3>Download files</h3> | ||
<ul id="fileList"></ul> | ||
<button onclick="fetchFiles()">Refresh</button> | ||
</div> | ||
|
||
<h3>OpenAPI spec: <a href="/openapi.json">/openapi.json</a></h3> | ||
<h3>API Explorer: <a href="/explorer">/explorer</a></h3> | ||
</div> | ||
|
||
<footer class="power"> | ||
<a href="https://loopback.io" target="_blank"> | ||
<img | ||
src="https://loopback.io/images/branding/powered-by-loopback/blue/powered-by-loopback-sm.png" | ||
/> | ||
</a> | ||
</footer> | ||
</body> | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#### Using @requestBody.file
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Other titles don't have
Using