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:
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
.
You can learn more about services and their naming convention in this documentation.
For example, create the file src/services/local-file.ts
with the following content:
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:
// ...
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:
You can access the plugin options in the second parameter passed to the constructor:
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. 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:
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:
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. 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:
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:
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:
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 beprivate
.
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:
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:
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:
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:
Test it Out
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 on how to test a plugin.
Run your backend to test it out:
Then, try uploading a file, for example, using the Upload File endpoint. The file should be uploaded based on the logic you’ve implemented.
(Optional) Accessing the File
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: