435 lines
13 KiB
Markdown
435 lines
13 KiB
Markdown
---
|
||
description: "Learn how to create a file service in a Medusa backend or a plugin."
|
||
---
|
||
|
||
# How to Create a File Service
|
||
|
||
In this document, you’ll learn how to create a file service in Medusa.
|
||
|
||
## Overview
|
||
|
||
In this guide, you’ll learn about the steps to implement a file service and the methods you’re required to implement in a file service. You can implement the file service within the Medusa backend codebase or in a plugin.
|
||
|
||
The file service you’ll be creating in this guide will be a local file service that allows you to upload files into your Medusa backend’s codebase. This is to give you a realistic example of the implementation of a file service. You’re free to implement the file service as required for your case.
|
||
|
||
---
|
||
|
||
## Prerequisites
|
||
|
||
### Multer Types Package
|
||
|
||
If you’re using TypeScript, as instructed by this guide, you should install the Multer types package to resolve errors within your file service types.
|
||
|
||
To do that, run the following command in the directory of your Medusa backend or plugin:
|
||
|
||
```bash npm2yarn
|
||
npm install @types/multer
|
||
```
|
||
|
||
---
|
||
|
||
## Step 1: Create the File Service Class
|
||
|
||
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, if you’re creating a local file service, the file name would be `local-file.ts`, whereas the class name would be `LocalFileService`.
|
||
|
||
:::tip
|
||
|
||
You can learn more about services and their naming convention in [this documentation](../services/overview.mdx).
|
||
|
||
:::
|
||
|
||
For example, create the file `src/services/local-file.ts` with the following content:
|
||
|
||
```ts title=src/services/local-file.ts
|
||
import {
|
||
AbstractFileService,
|
||
DeleteFileType,
|
||
FileServiceGetUploadStreamResult,
|
||
FileServiceUploadResult,
|
||
GetUploadedFileType,
|
||
UploadStreamDescriptorType,
|
||
} from "@medusajs/medusa"
|
||
|
||
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
|
||
```
|
||
|
||
This creates the service `LocalFileService` which, at the moment, adds a general implementation of the methods defined in the abstract class `AbstractFileService`.
|
||
|
||
### Using a Constructor
|
||
|
||
You can use a constructor to access services and resources registered in the dependency container, to define any necessary clients if you’re integrating a third-party storage service, and to access plugin options if your file service is defined in a plugin.
|
||
|
||
For example, the local service’s constructor could be useful to prepare the local upload directory:
|
||
|
||
```ts title=src/services/local-file.ts
|
||
// ...
|
||
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"
|
||
|
||
constructor(container) {
|
||
super(container)
|
||
|
||
// for public uploads
|
||
if (!fs.existsSync(this.publicPath)) {
|
||
fs.mkdirSync(this.publicPath)
|
||
}
|
||
|
||
// for protected uploads
|
||
if (!fs.existsSync(this.protectedPath)) {
|
||
fs.mkdirSync(this.protectedPath)
|
||
}
|
||
}
|
||
|
||
// ...
|
||
}
|
||
```
|
||
|
||
Another example showcasing how to access resources using dependency injection:
|
||
|
||
<!-- eslint-disable prefer-rest-params -->
|
||
|
||
```ts title=src/services/local-file.ts
|
||
type InjectedDependencies = {
|
||
logger: Logger
|
||
}
|
||
|
||
class LocalFileService extends AbstractFileService {
|
||
// ...
|
||
protected logger_: Logger
|
||
|
||
constructor({ logger }: InjectedDependencies) {
|
||
super(...arguments)
|
||
this.logger_ = logger
|
||
// ...
|
||
}
|
||
// ...
|
||
}
|
||
```
|
||
|
||
You can access the plugin options in the second parameter passed to the constructor:
|
||
|
||
```ts title=src/services/local-file.ts
|
||
class LocalFileService extends AbstractFileService {
|
||
protected serverUrl = "http://localhost:9000"
|
||
// ...
|
||
|
||
constructor(
|
||
container,
|
||
pluginOptions
|
||
) {
|
||
super(container)
|
||
|
||
if (pluginOptions?.serverUrl) {
|
||
this.serverUrl = pluginOptions?.serverUrl
|
||
}
|
||
// ...
|
||
}
|
||
// ...
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Step 2: Implement Required Methods
|
||
|
||
In this section, you’ll learn about the required methods to implement in the file service.
|
||
|
||
### upload
|
||
|
||
This method is used to upload a file to the Medusa backend. You must handle the upload logic within this method.
|
||
|
||
This method accepts one parameter, which is a [multer file object](http://expressjs.com/en/resources/middleware/multer.html#file-information). The 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.
|
||
|
||
So, for example, you can create a read stream to the file’s content if necessary using the `fs` library:
|
||
|
||
```ts
|
||
fs.createReadStream(file.path)
|
||
```
|
||
|
||
Where `file` is the parameter passed to the `upload` method.
|
||
|
||
The method is expected to return an object that has one property `url`, which is a string indicating the full accessible URL to the file.
|
||
|
||
An example implementation of this method for the local file service:
|
||
|
||
```ts title=src/services/local-file.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}`,
|
||
}
|
||
}
|
||
|
||
// ...
|
||
}
|
||
```
|
||
|
||
:::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.
|
||
|
||
:::
|
||
|
||
### 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.
|
||
|
||
You must handle the upload logic and the file permissions or private storage configuration within this method.
|
||
|
||
This method accepts one parameter, which is a [multer file object](http://expressjs.com/en/resources/middleware/multer.html#file-information). The 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.
|
||
|
||
So, for example, you can create a read stream to the file’s content if necessary using the `fs` library:
|
||
|
||
```ts
|
||
fs.createReadStream(file.path)
|
||
```
|
||
|
||
Where `file` is the parameter passed to the `uploadProtected` method.
|
||
|
||
The method is expected to return an object that has one property `url`, which is a string indicating the full accessible URL to the file.
|
||
|
||
An example implementation of this method for the local file service:
|
||
|
||
```ts title=src/services/local-file.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}`,
|
||
}
|
||
}
|
||
|
||
// ...
|
||
}
|
||
```
|
||
|
||
### delete
|
||
|
||
This method is used to delete a file from storage. You must handle the delete logic within this method.
|
||
This method accepts one parameter, which is an object that holds a `fileKey` property. The value of this property is a string that acts as an identifier of the file to delete. For example, for local file service, it could be the file name.
|
||
|
||
This method is not expected to return anything.
|
||
|
||
An example implementation of this method for the local file service:
|
||
|
||
```ts title=src/services/local-file.ts
|
||
class LocalFileService extends AbstractFileService {
|
||
|
||
async delete(
|
||
fileData: DeleteFileType
|
||
): Promise<void> {
|
||
fs.rmSync(fileData.fileKey)
|
||
}
|
||
|
||
// ...
|
||
}
|
||
```
|
||
|
||
### getUploadStreamDescriptor
|
||
|
||
This method is used to upload a file using a write stream. This is useful if the file is being written through a stream rather than uploaded to the temporary directory.
|
||
|
||
The method accepts one parameter, which is an object that has the following properties:
|
||
|
||
- `name`: a string indicating the name of the file.
|
||
- `ext`: an optional string indicating the extension of the file.
|
||
- `acl`: an optional string indicating the file’s permission. If the file should be uploaded privately, its value will be `private`.
|
||
|
||
The method is expected to return an object having the following properties:
|
||
|
||
- `writeStream`: a write stream object.
|
||
- `promise`: A promise that should resolved when the writing process is done to finish the upload. This depends on the type of file service you’re creating.
|
||
- `url`: a string indicating the URL of the file once it’s uploaded.
|
||
- `fileKey`: a string indicating the identifier of your file in the storage. For example, for a local file service this can be the file name.
|
||
|
||
An example implementation of this method for the local file service:
|
||
|
||
```ts title=src/services/local-file.ts
|
||
class LocalFileService extends AbstractFileService {
|
||
|
||
async getUploadStreamDescriptor(
|
||
fileData: UploadStreamDescriptorType
|
||
): Promise<FileServiceGetUploadStreamResult> {
|
||
const filePath = `${fileData.acl !== "private" ?
|
||
this.publicPath : this.protectedPath
|
||
}/${fileData.name}.${fileData.ext}`
|
||
const writeStream = fs.createWriteStream(filePath)
|
||
|
||
return {
|
||
writeStream,
|
||
promise: Promise.resolve(),
|
||
url: `${this.serverUrl}/${filePath}`,
|
||
fileKey: filePath,
|
||
}
|
||
}
|
||
|
||
// ...
|
||
}
|
||
```
|
||
|
||
### getDownloadStream
|
||
|
||
This method is used to read a file using a read stream, typically for download.
|
||
|
||
The method accepts as a parameter an object having the `fileKey` property, which is a string indicating the identifier of the file.
|
||
|
||
The method is expected to return a readable stream.
|
||
|
||
An example implementation of this method for the local file service:
|
||
|
||
```ts title=src/services/local-file.ts
|
||
class LocalFileService extends AbstractFileService {
|
||
|
||
async getDownloadStream(
|
||
fileData: GetUploadedFileType
|
||
): Promise<NodeJS.ReadableStream> {
|
||
const filePath = `${fileData.acl !== "private" ?
|
||
this.publicPath : this.protectedPath
|
||
}/${fileData.name}.${fileData.ext}`
|
||
const readStream = fs.createReadStream(filePath)
|
||
|
||
return readStream
|
||
}
|
||
|
||
// ...
|
||
}
|
||
```
|
||
|
||
### getPresignedDownloadUrl
|
||
|
||
The `getPresignedDownloadUrl` 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.
|
||
|
||
This method accepts as a parameter an object having the `fileKey` property, which is a string indicating the identifier of the file.
|
||
|
||
The method is expected to return a string, being the URL of the file.
|
||
|
||
An example implementation of this method for the local file service:
|
||
|
||
```ts title=src/services/local-file.ts
|
||
class LocalFileService extends AbstractFileService {
|
||
|
||
async getPresignedDownloadUrl(
|
||
fileData: GetUploadedFileType
|
||
): Promise<string> {
|
||
return `${this.serverUrl}/${fileData.fileKey}`
|
||
}
|
||
|
||
// ...
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Step 3: Run Build Command
|
||
|
||
In the directory of the Medusa backend, run the `build` command to transpile the files in the `src` directory into the `dist` directory:
|
||
|
||
```bash npm2yarn
|
||
npm run build
|
||
```
|
||
|
||
---
|
||
|
||
## Test it Out
|
||
|
||
:::note
|
||
|
||
This section explains how to test out your implementation if the file service was created in the Medusa backend codebase. You can refer to the [plugin documentation](../plugins/create.md#test-your-plugin) on how to test a plugin.
|
||
|
||
:::
|
||
|
||
Run your backend to test it out:
|
||
|
||
```bash npm2yarn
|
||
npx @medusajs/medusa-cli develop
|
||
```
|
||
|
||
Then, try uploading a file, for example, using the [Upload File endpoint](/api/admin#tag/Uploads/operation/PostUploads). The file should be uploaded based on the logic you’ve implemented.
|
||
|
||
### (Optional) Accessing the File
|
||
|
||
:::note
|
||
|
||
This step is only useful if you're implementing a local file service.
|
||
|
||
:::
|
||
|
||
Since the file is uploaded to a local directory `uploads`, you need to configure a static route in express that allows accessing the files within the `uploads` directory.
|
||
|
||
To do that, create the file `src/api/index.ts` with the following content:
|
||
|
||
```ts
|
||
import express from "express"
|
||
|
||
export default () => {
|
||
const app = express.Router()
|
||
|
||
app.use(`/uploads`, express.static(uploadDir))
|
||
|
||
return app
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## See Also
|
||
|
||
- [How to create a plugin](../plugins/create.md)
|
||
- [How to publish a plugin](../plugins/publish.md)
|