Generated the following references: - `file` - `fulfillment` - `inventory` - `js_client` - `medusa` - `medusa_config` - `medusa_react` - `modules` - `notification` - `payment` - `price_selection` - `pricing` - `product` - `services` - `stock_location` - `tax` - `tax_calculation` - `types` - `workflows` Co-authored-by: Shahed Nasser <27354907+shahednasser@users.noreply.github.com>
381 lines
16 KiB
Plaintext
381 lines
16 KiB
Plaintext
---
|
||
displayed_sidebar: core
|
||
slug: /development/file-service/create-file-service
|
||
---
|
||
|
||
import ParameterTypes from "@site/src/components/ParameterTypes"
|
||
|
||
# How to Create a File Service
|
||
|
||
In this document, you’ll learn how to create a file service in the Medusa backend and the methods you must implement in it.
|
||
|
||
## Overview
|
||
|
||
A file service class is defined in a TypeScript or JavaScript file that’s created in the `src/services` directory.
|
||
The class must extend the `AbstractFileService` class imported from the `@medusajs/medusa` package.
|
||
|
||
Based on services’ naming conventions, the file’s name should be the slug version of the file service’s name
|
||
without `service`, and the class’s name should be the pascal case of the file service’s name following by `Service`.
|
||
|
||
For example, create the file `src/services/local-file.ts` with the following content:
|
||
|
||
```ts title="src/services/local-file.ts"
|
||
import { AbstractFileService } from "@medusajs/medusa"
|
||
import {
|
||
DeleteFileType,
|
||
FileServiceGetUploadStreamResult,
|
||
FileServiceUploadResult,
|
||
GetUploadedFileType,
|
||
UploadStreamDescriptorType,
|
||
} from "@medusajs/types"
|
||
|
||
class LocalFileService extends AbstractFileService {
|
||
async upload(
|
||
fileData: Express.Multer.File
|
||
): Promise<FileServiceUploadResult> {
|
||
throw new Error("Method not implemented.")
|
||
}
|
||
async uploadProtected(
|
||
fileData: Express.Multer.File
|
||
): Promise<FileServiceUploadResult> {
|
||
throw new Error("Method not implemented.")
|
||
}
|
||
async delete(fileData: DeleteFileType): Promise<void> {
|
||
throw new Error("Method not implemented.")
|
||
}
|
||
async getUploadStreamDescriptor(
|
||
fileData: UploadStreamDescriptorType
|
||
): Promise<FileServiceGetUploadStreamResult> {
|
||
throw new Error("Method not implemented.")
|
||
}
|
||
async getDownloadStream(
|
||
fileData: GetUploadedFileType
|
||
): Promise<NodeJS.ReadableStream> {
|
||
throw new Error("Method not implemented.")
|
||
}
|
||
async getPresignedDownloadUrl(
|
||
fileData: GetUploadedFileType
|
||
): Promise<string> {
|
||
throw new Error("Method not implemented.")
|
||
}
|
||
}
|
||
|
||
export default LocalFileService
|
||
```
|
||
|
||
:::note[Multer Typing]
|
||
|
||
The examples implement a file service supporting local uploads.
|
||
|
||
If you’re using TypeScript and you're following along with the implementation,
|
||
you should install the Multer types package in the root of your Medusa backend to resolve errors within your file service types:
|
||
|
||
```bash npm2yarn
|
||
npm install @types/multer
|
||
```
|
||
|
||
:::
|
||
|
||
---
|
||
|
||
## constructor
|
||
|
||
You can use the `constructor` of your file service to access the different services in Medusa through dependency injection.
|
||
|
||
You can also use the constructor to initialize your integration with the third-party provider. For example, if you use a client to connect to the third-party provider’s APIs,
|
||
you can initialize it in the constructor and use it in other methods in the service.
|
||
|
||
Additionally, if you’re creating your file service as an external plugin to be installed on any Medusa backend and you want to access the options added for the plugin,
|
||
you can access them in the constructor.
|
||
|
||
### Example
|
||
|
||
```ts
|
||
// ...
|
||
import { Logger } from "@medusajs/medusa"
|
||
import * as fs from "fs"
|
||
|
||
class LocalFileService extends AbstractFileService {
|
||
// can also be replaced by an environment variable
|
||
// or a plugin option
|
||
protected serverUrl = "http://localhost:9000"
|
||
protected publicPath = "uploads"
|
||
protected protectedPath = "protected-uploads"
|
||
protected logger_: Logger
|
||
|
||
constructor({ logger }: InjectedDependencies) {
|
||
// @ts-ignore
|
||
super(...arguments)
|
||
this.logger_ = logger
|
||
|
||
// for public uploads
|
||
if (!fs.existsSync(this.publicPath)) {
|
||
fs.mkdirSync(this.publicPath)
|
||
}
|
||
|
||
// for protected uploads
|
||
if (!fs.existsSync(this.protectedPath)) {
|
||
fs.mkdirSync(this.protectedPath)
|
||
}
|
||
}
|
||
// ...
|
||
}
|
||
```
|
||
|
||
### Parameters
|
||
|
||
<ParameterTypes parameters={[{"name":"container","type":"`Record<string, unknown>`","description":"An instance of `MedusaContainer` that allows you to access other resources, such as services, in your Medusa backend.","optional":false,"defaultValue":"","expandable":false,"children":[]},{"name":"config","type":"`Record<string, unknown>`","description":"If this file service is created in a plugin, the plugin's options are passed in this parameter.","optional":true,"defaultValue":"","expandable":false,"children":[]}]} sectionTitle="new AbstractFileService"/>
|
||
|
||
___
|
||
|
||
## Methods
|
||
|
||
### upload
|
||
|
||
This method is used to upload a file to the Medusa backend.
|
||
|
||
#### Example
|
||
|
||
```ts
|
||
class LocalFileService extends AbstractFileService {
|
||
// ...
|
||
async upload(
|
||
fileData: Express.Multer.File
|
||
): Promise<FileServiceUploadResult> {
|
||
const filePath =
|
||
`${this.publicPath}/${fileData.originalname}`
|
||
fs.copyFileSync(fileData.path, filePath)
|
||
return {
|
||
url: `${this.serverUrl}/${filePath}`,
|
||
key: filePath,
|
||
}
|
||
}
|
||
// ...
|
||
}
|
||
```
|
||
|
||
:::tip
|
||
|
||
This example does not account for duplicate names to maintain simplicity in this guide. So, an uploaded file can replace another existing file that has the same name.
|
||
|
||
:::
|
||
|
||
#### Parameters
|
||
|
||
<ParameterTypes parameters={[{"name":"fileData","type":"`File`","description":"A [multer file object](http://expressjs.com/en/resources/middleware/multer.html#file-information).\nThe file is uploaded to a temporary directory by default. Among the file’s details, you can access the file’s path in the `path` property of the file object.","optional":false,"defaultValue":"","expandable":false,"children":[]}]} sectionTitle="upload"/>
|
||
|
||
#### Returns
|
||
|
||
<ParameterTypes parameters={[{"name":"Promise","type":"Promise<[FileServiceUploadResult](../../medusa/interfaces/medusa.FileServiceUploadResult.mdx)>","optional":false,"defaultValue":"","description":"The details of the upload's result.","expandable":false,"children":[{"name":"FileServiceUploadResult","type":"[FileServiceUploadResult](../../medusa/interfaces/medusa.FileServiceUploadResult.mdx)","optional":false,"defaultValue":"","description":"Details of a file upload's result.","expandable":false,"children":[{"name":"url","type":"`string`","description":"The file's URL.","optional":false,"defaultValue":"","expandable":false,"children":[]},{"name":"key","type":"`string`","description":"The file's key. This key is used in other operations,\nsuch as deleting a file.","optional":false,"defaultValue":"","expandable":false,"children":[]}]}]}]} sectionTitle="upload"/>
|
||
|
||
### uploadProtected
|
||
|
||
This method is used to upload a file to the Medusa backend, but to a protected storage. Typically, this would be used to store files that
|
||
shouldn’t be accessible by using the file’s URL or should only be accessible by authenticated users. For example, exported or imported
|
||
CSV files.
|
||
|
||
#### Example
|
||
|
||
```ts
|
||
class LocalFileService extends AbstractFileService {
|
||
// ...
|
||
async uploadProtected(
|
||
fileData: Express.Multer.File
|
||
): Promise<FileServiceUploadResult> {
|
||
const filePath =
|
||
`${this.protectedPath}/${fileData.originalname}`
|
||
fs.copyFileSync(fileData.path, filePath)
|
||
return {
|
||
url: `${this.serverUrl}/${filePath}`,
|
||
key: filePath
|
||
}
|
||
}
|
||
// ...
|
||
}
|
||
```
|
||
|
||
:::tip
|
||
|
||
This example does not account for duplicate names to maintain simplicity in this guide. So, an uploaded file can replace another existing file that has the same name.
|
||
|
||
:::
|
||
|
||
#### Parameters
|
||
|
||
<ParameterTypes parameters={[{"name":"fileData","type":"`File`","description":"A [multer file object](http://expressjs.com/en/resources/middleware/multer.html#file-information).\nThe file is uploaded to a temporary directory by default. Among the file’s details, you can access the file’s path in the `path` property of the file object.","optional":false,"defaultValue":"","expandable":false,"children":[]}]} sectionTitle="uploadProtected"/>
|
||
|
||
#### Returns
|
||
|
||
<ParameterTypes parameters={[{"name":"Promise","type":"Promise<[FileServiceUploadResult](../../medusa/interfaces/medusa.FileServiceUploadResult.mdx)>","optional":false,"defaultValue":"","description":"The details of the upload's result.","expandable":false,"children":[{"name":"FileServiceUploadResult","type":"[FileServiceUploadResult](../../medusa/interfaces/medusa.FileServiceUploadResult.mdx)","optional":false,"defaultValue":"","description":"Details of a file upload's result.","expandable":false,"children":[{"name":"url","type":"`string`","description":"The file's URL.","optional":false,"defaultValue":"","expandable":false,"children":[]},{"name":"key","type":"`string`","description":"The file's key. This key is used in other operations,\nsuch as deleting a file.","optional":false,"defaultValue":"","expandable":false,"children":[]}]}]}]} sectionTitle="uploadProtected"/>
|
||
|
||
### delete
|
||
|
||
This method is used to delete a file from storage.
|
||
|
||
#### Example
|
||
|
||
```ts
|
||
class LocalFileService extends AbstractFileService {
|
||
|
||
async delete(
|
||
fileData: DeleteFileType
|
||
): Promise<void> {
|
||
fs.rmSync(fileData.fileKey)
|
||
}
|
||
|
||
// ...
|
||
}
|
||
```
|
||
|
||
#### Parameters
|
||
|
||
<ParameterTypes parameters={[{"name":"fileData","type":"[DeleteFileType](../../medusa/interfaces/medusa.DeleteFileType.mdx)","description":"The details of the file to remove.","optional":false,"defaultValue":"","expandable":false,"children":[{"name":"fileKey","type":"`string`","description":"The file's key. When uploading a file, the\nreturned key is used here.","optional":false,"defaultValue":"","expandable":false,"children":[]}]}]} sectionTitle="delete"/>
|
||
|
||
#### Returns
|
||
|
||
<ParameterTypes parameters={[{"name":"Promise","type":"Promise<void>","optional":false,"defaultValue":"","description":"Resolves when the file is deleted successfully.","expandable":false,"children":[]}]} sectionTitle="delete"/>
|
||
|
||
### getUploadStreamDescriptor
|
||
|
||
This method is used to retrieve a write stream to be used to upload a file.
|
||
|
||
#### Example
|
||
|
||
```ts
|
||
// ...
|
||
import { Stream } from "stream"
|
||
|
||
class LocalFileService extends AbstractFileService {
|
||
// ...
|
||
async getUploadStreamDescriptor({
|
||
name,
|
||
ext,
|
||
isPrivate = true,
|
||
}: UploadStreamDescriptorType
|
||
): Promise<FileServiceGetUploadStreamResult> {
|
||
const filePath = `${isPrivate ?
|
||
this.publicPath : this.protectedPath
|
||
}/${name}.${ext}`
|
||
|
||
const pass = new Stream.PassThrough()
|
||
const writeStream = fs.createWriteStream(filePath)
|
||
|
||
pass.pipe(writeStream)
|
||
|
||
return {
|
||
writeStream: pass,
|
||
promise: Promise.resolve(),
|
||
url: `${this.serverUrl}/${filePath}`,
|
||
fileKey: filePath,
|
||
}
|
||
}
|
||
// ...
|
||
}
|
||
```
|
||
|
||
#### Parameters
|
||
|
||
<ParameterTypes parameters={[{"name":"fileData","type":"[UploadStreamDescriptorType](../../medusa/interfaces/medusa.UploadStreamDescriptorType.mdx)","description":"The details of the file being uploaded.","optional":false,"defaultValue":"","expandable":false,"children":[{"name":"name","type":"`string`","description":"The name of the file.","optional":false,"defaultValue":"","expandable":false,"children":[]},{"name":"ext","type":"`string`","description":"The extension of the file.","optional":true,"defaultValue":"","expandable":false,"children":[]},{"name":"isPrivate","type":"`boolean`","description":"Whether the file should be uploaded to a private bucket or location. By convention, the default value of this property is `true`.","optional":true,"defaultValue":"","expandable":false,"children":[]}]}]} sectionTitle="getUploadStreamDescriptor"/>
|
||
|
||
#### Returns
|
||
|
||
<ParameterTypes parameters={[{"name":"Promise","type":"Promise<[FileServiceGetUploadStreamResult](../../medusa/interfaces/medusa.FileServiceGetUploadStreamResult.mdx)>","optional":false,"defaultValue":"","description":"The result of the file-stream upload.","expandable":false,"children":[{"name":"FileServiceGetUploadStreamResult","type":"[FileServiceGetUploadStreamResult](../../medusa/interfaces/medusa.FileServiceGetUploadStreamResult.mdx)","optional":false,"defaultValue":"","description":"The relevant details to upload a file through a stream.","expandable":false,"children":[{"name":"writeStream","type":"`PassThrough`","description":"A [PassThrough](https://nodejs.org/api/stream.html#class-streampassthrough) write stream object to be used to write the file.","optional":false,"defaultValue":"","expandable":false,"children":[]},{"name":"promise","type":"Promise<any>","description":"A promise that should resolved when the writing process is done to finish the upload.","optional":false,"defaultValue":"","expandable":false,"children":[]},{"name":"url","type":"`string`","description":"The URL of the file once it’s uploaded.","optional":false,"defaultValue":"","expandable":false,"children":[]},{"name":"fileKey","type":"`string`","description":"The identifier of the file in the storage. For example, for a local file service, this can be the file's name.","optional":false,"defaultValue":"","expandable":false,"children":[]}]}]}]} sectionTitle="getUploadStreamDescriptor"/>
|
||
|
||
### getDownloadStream
|
||
|
||
This method is used to retrieve a read stream for a file, which can then be used to download the file.
|
||
|
||
#### Example
|
||
|
||
```ts
|
||
class LocalFileService extends AbstractFileService {
|
||
|
||
async getDownloadStream({
|
||
fileKey,
|
||
isPrivate = true,
|
||
}: GetUploadedFileType
|
||
): Promise<NodeJS.ReadableStream> {
|
||
const filePath = `${isPrivate ?
|
||
this.publicPath : this.protectedPath
|
||
}/${fileKey}`
|
||
const readStream = fs.createReadStream(filePath)
|
||
|
||
return readStream
|
||
}
|
||
|
||
// ...
|
||
}
|
||
```
|
||
|
||
#### Parameters
|
||
|
||
<ParameterTypes parameters={[{"name":"fileData","type":"[GetUploadedFileType](../../medusa/interfaces/medusa.GetUploadedFileType.mdx)","description":"The details of the file.","optional":false,"defaultValue":"","expandable":false,"children":[{"name":"fileKey","type":"`string`","description":"The file's key.","optional":false,"defaultValue":"","expandable":false,"children":[]},{"name":"isPrivate","type":"`boolean`","description":"Whether the file is private.","optional":true,"defaultValue":"","expandable":false,"children":[]}]}]} sectionTitle="getDownloadStream"/>
|
||
|
||
#### Returns
|
||
|
||
<ParameterTypes parameters={[{"name":"Promise","type":"Promise<ReadableStream>","optional":false,"defaultValue":"","description":"The [read stream](https://nodejs.org/api/webstreams.html#class-readablestream) to read and download the file.","expandable":false,"children":[{"name":"ReadableStream","type":"`ReadableStream`","optional":false,"defaultValue":"","description":"","expandable":false,"children":[]}]}]} sectionTitle="getDownloadStream"/>
|
||
|
||
### getPresignedDownloadUrl
|
||
|
||
This method is used to retrieve a download URL of the file. For some file services, such as S3, a presigned URL indicates a temporary URL to get access to a file.
|
||
|
||
If your file service doesn’t perform or offer a similar functionality, you can just return the URL to download the file.
|
||
|
||
#### Example
|
||
|
||
```ts
|
||
class LocalFileService extends AbstractFileService {
|
||
|
||
async getPresignedDownloadUrl({
|
||
fileKey,
|
||
isPrivate = true,
|
||
}: GetUploadedFileType
|
||
): Promise<string> {
|
||
// Local upload doesn't provide
|
||
// support for presigned URLs,
|
||
// so just return the file's URL.
|
||
|
||
const filePath = `${isPrivate ?
|
||
this.publicPath : this.protectedPath
|
||
}/${fileKey}`
|
||
return `${this.serverUrl}/${filePath}`
|
||
}
|
||
|
||
// ...
|
||
}
|
||
```
|
||
|
||
#### Parameters
|
||
|
||
<ParameterTypes parameters={[{"name":"fileData","type":"[GetUploadedFileType](../../medusa/interfaces/medusa.GetUploadedFileType.mdx)","description":"The details of the file.","optional":false,"defaultValue":"","expandable":false,"children":[{"name":"fileKey","type":"`string`","description":"The file's key.","optional":false,"defaultValue":"","expandable":false,"children":[]},{"name":"isPrivate","type":"`boolean`","description":"Whether the file is private.","optional":true,"defaultValue":"","expandable":false,"children":[]}]}]} sectionTitle="getPresignedDownloadUrl"/>
|
||
|
||
#### Returns
|
||
|
||
<ParameterTypes parameters={[{"name":"Promise","type":"Promise<string>","optional":false,"defaultValue":"","description":"The presigned URL to download the file","expandable":false,"children":[{"name":"string","type":"`string`","optional":false,"defaultValue":"","description":"","expandable":false,"children":[]}]}]} sectionTitle="getPresignedDownloadUrl"/>
|
||
|
||
---
|
||
|
||
## Test Implementation
|
||
|
||
:::note
|
||
|
||
If you created your file service in a plugin, refer to [this guide on how to test plugins](https://docs.medusajs.com/development/plugins/create#test-your-plugin).
|
||
|
||
:::
|
||
|
||
After finishing your file service implementation:
|
||
|
||
1\. Run the `build` command in the root of your Medusa backend:
|
||
|
||
```bash npm2yarn
|
||
npm run build
|
||
```
|
||
|
||
2\. Start the backend with the `develop` command:
|
||
|
||
```bash
|
||
npx medusa develop
|
||
```
|
||
|
||
3\. Upload a file using the [Admin REST APIs](https://docs.medusajs.com/api/admin#uploads_postuploads) or using the Medusa admin, for example, to [upload a product's thumbnail](https://docs.medusajs.com/user-guide/products/manage#manage-thumbnails).
|