feat: implement direct upload (#12328)

* feat: implement direct upload

* feat: add direct-upload endpoint

* refactor: implement feedback

* refactor: have a dedicated endpoint for direct uploads

* refactor: convert responses to snakecase

* refactor: rename method to createImport

* test: add tests for the presigned-urls endpoint
This commit is contained in:
Harminder Virk
2025-05-04 12:58:10 +02:00
committed by GitHub
parent ed55d17a36
commit 84f58f9058
22 changed files with 537 additions and 129 deletions
@@ -167,8 +167,8 @@ export default class FileModuleService implements FileTypes.IFileModuleService {
* const stream = await fileModuleService.getAsStream("file_123")
* writeable.pipe(stream)
*/
getAsStream(id: string): Promise<Readable> {
return this.fileProviderService_.getAsStream({ fileKey: id })
getDownloadStream(id: string): Promise<Readable> {
return this.fileProviderService_.getDownloadStream({ fileKey: id })
}
/**
@@ -70,8 +70,8 @@ export default class FileProviderService {
return this.fileProvider_.getPresignedUploadUrl(fileData)
}
getAsStream(fileData: FileTypes.ProviderGetFileDTO): Promise<Readable> {
return this.fileProvider_.getAsStream(fileData)
getDownloadStream(fileData: FileTypes.ProviderGetFileDTO): Promise<Readable> {
return this.fileProvider_.getDownloadStream(fileData)
}
getAsBuffer(fileData: FileTypes.ProviderGetFileDTO): Promise<Buffer> {
@@ -85,7 +85,9 @@ export class LocalFileService extends AbstractFileProviderService {
return
}
async getAsStream(file: FileTypes.ProviderGetFileDTO): Promise<Readable> {
async getDownloadStream(
file: FileTypes.ProviderGetFileDTO
): Promise<Readable> {
const baseDir = file.fileKey.startsWith("private-")
? this.privateUploadDir_
: this.uploadDir_
@@ -124,6 +126,47 @@ export class LocalFileService extends AbstractFileProviderService {
return this.getUploadFileUrl(file.fileKey)
}
/**
* Returns the pre-signed URL that the client (frontend) can use to trigger
* a file upload. In this case, the Medusa backend will implement the
* "/upload" endpoint to perform the file upload.
*
* Since, we do not want the client to perform link detection on the frontend
* and then prepare a different kind of request for cloud providers and different
* request for the local server, we will have to make these URLs self sufficient.
*
* What is a self sufficient URL
*
* - There should be no need to specify the MIME type or filename separately in request body (cloud providers don't allow it).
* - There should be no need to pass auth headers like cookies. Again cloud providers
* won't allow it and will likely result in a CORS error.
*/
async getPresignedUploadUrl(
fileData: FileTypes.ProviderGetPresignedUploadUrlDTO
): Promise<FileTypes.ProviderFileResultDTO> {
if (!fileData?.filename) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`No filename provided`
)
}
const uploadUrl = new URL(
"upload",
`${this.backendUrl_.replace(/\/$/, "")}/`
)
uploadUrl.searchParams.set("filename", fileData.filename)
if (fileData.mimeType) {
uploadUrl.searchParams.set("type", fileData.mimeType)
}
return {
url: uploadUrl.toString(),
key: fileData.filename,
}
}
private getUploadFilePath = (baseDir: string, fileKey: string) => {
return path.join(baseDir, fileKey)
}
@@ -217,15 +217,17 @@ export class S3FileService extends AbstractFileProviderService {
}
}
async getAsStream(file: FileTypes.ProviderGetFileDTO): Promise<Readable> {
if (!file?.filename) {
async getDownloadStream(
file: FileTypes.ProviderGetFileDTO
): Promise<Readable> {
if (!file?.fileKey) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`No filename provided`
`No fileKey provided`
)
}
const fileKey = `${this.config_.prefix}${file.filename}`
const fileKey = `${this.config_.prefix}${file.fileKey}`
const response = await this.client_.send(
new GetObjectCommand({
Key: fileKey,
@@ -237,14 +239,14 @@ export class S3FileService extends AbstractFileProviderService {
}
async getAsBuffer(file: FileTypes.ProviderGetFileDTO): Promise<Buffer> {
if (!file?.filename) {
if (!file?.fileKey) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`No filename provided`
`No fileKey provided`
)
}
const fileKey = `${this.config_.prefix}${file.filename}`
const fileKey = `${this.config_.prefix}${file.fileKey}`
const response = await this.client_.send(
new GetObjectCommand({
Key: fileKey,