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:
1 parent
ed55d17a36
commit
84f58f9058
22 files changed
+435
-27
No files matched your search
@@ -0,0 +1,5 @@
|
||||
Product Id,Product Handle,Product Title,Product Status,Product Description,Product Subtitle,Product External Id,Product Thumbnail,Product Collection Id,Product Type Id,Product Category 1,Product Created At,Product Deleted At,Product Discountable,Product Height,Product Hs Code,Product Image 1,Product Image 2,Product Is Giftcard,Product Length,Product Material,Product Mid Code,Product Origin Country,Product Tag 1,Product Tag 2,Product Updated At,Product Weight,Product Width,Variant Id,Variant Title,Variant Sku,Variant Upc,Variant Ean,Variant Hs Code,Variant Mid Code,Variant Manage Inventory,Variant Allow Backorder,Variant Barcode,Variant Created At,Variant Deleted At,Variant Height,Variant Length,Variant Material,Variant Metadata,Variant Option 1 Name,Variant Option 1 Value,Variant Option 2 Name,Variant Option 2 Value,Variant Origin Country,Variant Price DKK,Variant Price EUR,Variant Price USD,Variant Product Id,Variant Updated At,Variant Variant Rank,Variant Weight,Variant Width,Shipping Profile Id
|
||||
prod_01J44RRKH4HH2SANJ0S05YM853,base-product,Base product,draft,"test-product-description
|
||||
test line 2",,,test-image.png,pcol_01J44RRKG4P1RZ3CKAMBS760TD,ptyp_01J44RRKGHPQMJ1CRMW7MEN3P1,pcat_01J44RRKGS2N86A8V37ZJD877K,2024-07-31T16:07:55.681Z,,true,,,test-image.png,test-image-2.png,false,,,,,123,456,2024-07-31T16:07:55.681Z,,,variant_01J44RRKHRQ7WJ902X4936GEK7,Test variant,,,,,,true,false,,2024-07-31T16:07:55.704Z,,,,,,size,large,color,green,,30,45,100,prod_01J44RRKH4HH2SANJ0S05YM853,2024-07-31T16:07:55.704Z,0,,,import-shipping-profile
|
||||
prod_01J44RRKH4HH2SANJ0S05YM853,base-product,Base product,draft,"test-product-description
|
||||
test line 2",,,test-image.png,pcol_01J44RRKG4P1RZ3CKAMBS760TD,ptyp_01J44RRKGHPQMJ1CRMW7MEN3P1,pcat_01J44RRKGS2N86A8V37ZJD877K,2024-07-31T16:07:55.681Z,,true,,,test-image.png,test-image-2.png,false,,,,,123,456,2024-07-31T16:07:55.681Z,,,variant_01J44RRKHRAYY7Q7NTXNNMDA1S,Test variant 2,,,,,,true,false,,2024-07-31T16:07:55.704Z,,,,,,size,small,color,green,,50,65,200,prod_01J44RRKH4HH2SANJ0S05YM853,2024-07-31T16:07:55.704Z,0,,,import-shipping-profile
|
||||
|
@@ -0,0 +1,84 @@
|
||||
import { join } from "path"
|
||||
import { readFile } from "fs/promises"
|
||||
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
|
||||
import {
|
||||
adminHeaders,
|
||||
createAdminUser,
|
||||
} from "../../../../helpers/create-admin-user"
|
||||
import { AdminUploadPreSignedUrlRequest } from "@medusajs/types"
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
const PRODUCTS_FILE_PATH = join(__dirname, "./__fixtures__", "products.csv")
|
||||
const getUploadReq = (file: File) => {
|
||||
return {
|
||||
body: {
|
||||
mime_type: file.type,
|
||||
originalname: file.name,
|
||||
size: file.size,
|
||||
access: "public",
|
||||
} satisfies AdminUploadPreSignedUrlRequest,
|
||||
meta: {
|
||||
headers: {
|
||||
...adminHeaders.headers,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
medusaIntegrationTestRunner({
|
||||
testSuite: ({ dbConnection, getContainer, api }) => {
|
||||
beforeEach(async () => {
|
||||
await createAdminUser(dbConnection, adminHeaders, getContainer())
|
||||
})
|
||||
|
||||
describe("POST /admin/uploads/presigned-urls", () => {
|
||||
it("should generate a signed URL to upload a file", async () => {
|
||||
const file = new File(
|
||||
[await readFile(PRODUCTS_FILE_PATH)],
|
||||
"products.csv",
|
||||
{
|
||||
type: "text/csv",
|
||||
}
|
||||
)
|
||||
const { body, meta } = getUploadReq(file)
|
||||
|
||||
const response = await api.post(
|
||||
"/admin/uploads/presigned-urls",
|
||||
body,
|
||||
meta
|
||||
)
|
||||
|
||||
expect(response.data).toEqual(
|
||||
expect.objectContaining({
|
||||
filename: expect.stringContaining(".csv"),
|
||||
extension: "csv",
|
||||
mime_type: "text/csv",
|
||||
size: file.size,
|
||||
url: expect.stringContaining(response.data.filename),
|
||||
})
|
||||
)
|
||||
expect(response.status).toEqual(200)
|
||||
})
|
||||
|
||||
it("should return error when mime type is invalid", async () => {
|
||||
const file = new File(
|
||||
[await readFile(PRODUCTS_FILE_PATH)],
|
||||
"products.csv"
|
||||
)
|
||||
const { body, meta } = getUploadReq(file)
|
||||
|
||||
try {
|
||||
await api.post("/admin/uploads/presigned-urls", body, meta)
|
||||
} catch (error) {
|
||||
expect(error.response.data).toEqual({
|
||||
code: "invalid_data",
|
||||
type: "invalid_data",
|
||||
message: 'Invalid file type ""',
|
||||
})
|
||||
expect(error.response.status).toEqual(400)
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -58,6 +58,82 @@ export class Product {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* This method creates a product import. The products are only imported after
|
||||
* the import is confirmed using the {@link confirmImport} method.
|
||||
*
|
||||
* This method sends a request to the
|
||||
* [Create Product Import](https://docs.medusajs.com/api/admin#products_postproductsimport)
|
||||
* API route.
|
||||
*
|
||||
* @param body - The import's details.
|
||||
* @param query - Query parameters to pass to the request.
|
||||
* @param headers - Headers to pass in the request.
|
||||
* @returns The import's details.
|
||||
*
|
||||
* @example
|
||||
* sdk.admin.product.createImport({
|
||||
* file // uploaded File instance
|
||||
* })
|
||||
* .then(({ transaction_id }) => {
|
||||
* console.log(transaction_id)
|
||||
* })
|
||||
*/
|
||||
async createImport(
|
||||
body: HttpTypes.AdminImportProductRequest,
|
||||
query?: {},
|
||||
headers?: ClientHeaders
|
||||
) {
|
||||
/**
|
||||
* Get signed URL for file uploads
|
||||
*/
|
||||
const response =
|
||||
await this.client.fetch<HttpTypes.AdminUploadPreSignedUrlResponse>(
|
||||
"admin/uploads/presigned-urls",
|
||||
{
|
||||
method: "POST",
|
||||
headers: headers,
|
||||
body: {
|
||||
originalname: body.file.name,
|
||||
mime_type: body.file.type,
|
||||
size: body.file.size,
|
||||
} satisfies HttpTypes.AdminUploadPreSignedUrlRequest,
|
||||
query,
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Upload file using the signed URL. We cannot send cookies or any other
|
||||
* special headers in this request, since external services like S3 will
|
||||
* give a CORS error.
|
||||
*/
|
||||
await fetch(response.url, {
|
||||
method: "PUT",
|
||||
body: body.file,
|
||||
})
|
||||
|
||||
/**
|
||||
* Perform products import using the uploaded file name
|
||||
*/
|
||||
return await this.client.fetch<HttpTypes.AdminImportProductsResponse>(
|
||||
"/admin/products/imports",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
...headers,
|
||||
},
|
||||
body: {
|
||||
file_key: response.filename,
|
||||
originalname: response.originalname,
|
||||
extension: response.extension,
|
||||
size: response.size,
|
||||
mime_type: response.mime_type,
|
||||
} satisfies HttpTypes.AdminImportProductsRequest,
|
||||
query,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* This method confirms a product import created using the method {@link import}.
|
||||
* It sends a request to the
|
||||
|
||||
@@ -13,7 +13,12 @@ export type Config = {
|
||||
auth?: {
|
||||
type?: "jwt" | "session"
|
||||
jwtTokenStorageKey?: string
|
||||
jwtTokenStorageMethod?: "local" | "session" | "memory" | "custom" | "nostore"
|
||||
jwtTokenStorageMethod?:
|
||||
| "local"
|
||||
| "session"
|
||||
| "memory"
|
||||
| "custom"
|
||||
| "nostore"
|
||||
fetchCredentials?: "include" | "omit" | "same-origin"
|
||||
storage?: CustomStorage
|
||||
}
|
||||
|
||||
@@ -149,7 +149,7 @@ export interface IFileProvider {
|
||||
/**
|
||||
* Get the file contents as a readable stream.
|
||||
*/
|
||||
getAsStream(fileData: ProviderGetFileDTO): Promise<Readable>
|
||||
getDownloadStream(fileData: ProviderGetFileDTO): Promise<Readable>
|
||||
|
||||
/**
|
||||
* Get the file contents as a Node.js Buffer
|
||||
|
||||
@@ -169,10 +169,10 @@ export interface IFileModuleService extends IModuleService {
|
||||
* Get the file contents as a readable stream.
|
||||
*
|
||||
* @example
|
||||
* const stream = await fileModuleService.getAsStream("file_123")
|
||||
* const stream = await fileModuleService.getDownloadStream("file_123")
|
||||
* writeable.pipe(stream)
|
||||
*/
|
||||
getAsStream(id: string, sharedContext?: Context): Promise<Readable>
|
||||
getDownloadStream(id: string, sharedContext?: Context): Promise<Readable>
|
||||
|
||||
/**
|
||||
* Get the file contents as a Node.js Buffer
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
import { BaseUploadFile } from "../common"
|
||||
|
||||
export type AdminUploadFile = BaseUploadFile
|
||||
|
||||
export interface AdminUploadPreSignedUrlRequest {
|
||||
originalname: string
|
||||
mime_type: string
|
||||
size: number
|
||||
access?: "public" | "private"
|
||||
}
|
||||
@@ -16,3 +16,36 @@ export interface AdminFileListResponse {
|
||||
}
|
||||
|
||||
export type AdminFileDeleteResponse = DeleteResponse<"file">
|
||||
|
||||
export interface AdminUploadPreSignedUrlResponse {
|
||||
/**
|
||||
* The URL to be used for uploading the file
|
||||
*/
|
||||
url: string
|
||||
|
||||
/**
|
||||
* The unique filename that can be used with the file provider
|
||||
* to fetch the file
|
||||
*/
|
||||
filename: string
|
||||
|
||||
/**
|
||||
* The original name of the file on the user's computer (aka clientName)
|
||||
*/
|
||||
originalname: string
|
||||
|
||||
/**
|
||||
* The mime type of the file.
|
||||
*/
|
||||
mime_type: string
|
||||
|
||||
/**
|
||||
* Extension of the file to be uploaded
|
||||
*/
|
||||
extension: string
|
||||
|
||||
/**
|
||||
* Size of the file to be uploaded (in bytes)
|
||||
*/
|
||||
size: number
|
||||
}
|
||||
@@ -570,3 +570,11 @@ interface AdminDeleteProductVariantInventoryItem {
|
||||
*/
|
||||
variant_id: string
|
||||
}
|
||||
|
||||
export interface AdminImportProductsRequest {
|
||||
file_key: string
|
||||
originalname: string
|
||||
extension: string
|
||||
size: number
|
||||
mime_type: string
|
||||
}
|
||||
@@ -73,6 +73,9 @@ export interface AdminImportProductResponse {
|
||||
}
|
||||
}
|
||||
|
||||
export interface AdminImportProductsResponse
|
||||
extends AdminImportProductResponse {}
|
||||
|
||||
export interface AdminBatchProductVariantResponse
|
||||
extends BatchResponse<AdminProductVariant> {}
|
||||
|
||||
|
||||
@@ -187,8 +187,8 @@ export class AbstractFileProviderService implements IFileProvider {
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
getAsStream(fileData: FileTypes.ProviderGetFileDTO): Promise<Readable> {
|
||||
throw Error("getAsStream must be overridden by the child class")
|
||||
getDownloadStream(fileData: FileTypes.ProviderGetFileDTO): Promise<Readable> {
|
||||
throw Error("getDownloadStream must be overridden by the child class")
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import {
|
||||
AuthenticatedMedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/framework/http"
|
||||
|
||||
import {
|
||||
importProductsWorkflowId,
|
||||
waitConfirmationProductImportStepId,
|
||||
} from "@medusajs/core-flows"
|
||||
import { IWorkflowEngineService } from "@medusajs/framework/types"
|
||||
import { Modules, TransactionHandlerType } from "@medusajs/framework/utils"
|
||||
import { StepResponse } from "@medusajs/framework/workflows-sdk"
|
||||
|
||||
export const POST = async (
|
||||
req: AuthenticatedMedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const workflowEngineService: IWorkflowEngineService = req.scope.resolve(
|
||||
Modules.WORKFLOW_ENGINE
|
||||
)
|
||||
const transactionId = req.params.transaction_id
|
||||
|
||||
await workflowEngineService.setStepSuccess({
|
||||
idempotencyKey: {
|
||||
action: TransactionHandlerType.INVOKE,
|
||||
transactionId,
|
||||
stepId: waitConfirmationProductImportStepId,
|
||||
workflowId: importProductsWorkflowId,
|
||||
},
|
||||
stepResponse: new StepResponse(true),
|
||||
})
|
||||
|
||||
res.status(202).json({})
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import {
|
||||
MedusaResponse,
|
||||
AuthenticatedMedusaRequest,
|
||||
} from "@medusajs/framework/http"
|
||||
import { Modules } from "@medusajs/framework/utils"
|
||||
import type { HttpTypes } from "@medusajs/framework/types"
|
||||
import { importProductsWorkflow } from "@medusajs/core-flows"
|
||||
import type { AdminImportProductsType } from "../validators"
|
||||
|
||||
export const POST = async (
|
||||
req: AuthenticatedMedusaRequest<AdminImportProductsType>,
|
||||
res: MedusaResponse<HttpTypes.AdminImportProductResponse>
|
||||
) => {
|
||||
const fileProvider = req.scope.resolve(Modules.FILE)
|
||||
const file = await fileProvider.getAsBuffer(req.validatedBody.file_key)
|
||||
|
||||
const { result, transaction } = await importProductsWorkflow(req.scope).run({
|
||||
input: {
|
||||
filename: req.validatedBody.originalname,
|
||||
fileContent: file.toString("utf-8"),
|
||||
},
|
||||
})
|
||||
|
||||
res
|
||||
.status(202)
|
||||
.json({ transaction_id: transaction.transactionId, summary: result })
|
||||
}
|
||||
@@ -3,8 +3,8 @@ import {
|
||||
validateAndTransformBody,
|
||||
validateAndTransformQuery,
|
||||
} from "@medusajs/framework"
|
||||
import { maybeApplyLinkFilter, MiddlewareRoute } from "@medusajs/framework/http"
|
||||
import multer from "multer"
|
||||
import { maybeApplyLinkFilter, MiddlewareRoute } from "@medusajs/framework/http"
|
||||
import { DEFAULT_BATCH_ENDPOINTS_SIZE_LIMIT } from "../../../utils/middlewares"
|
||||
import { createBatchBody } from "../../utils/validators"
|
||||
import * as QueryConfig from "./query-config"
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
AdminGetProductsParams,
|
||||
AdminGetProductVariantParams,
|
||||
AdminGetProductVariantsParams,
|
||||
AdminImportProducts,
|
||||
AdminUpdateProduct,
|
||||
AdminUpdateProductOption,
|
||||
AdminUpdateProductVariant,
|
||||
@@ -34,9 +35,6 @@ import {
|
||||
} from "./validators"
|
||||
import IndexEngineFeatureFlag from "../../../loaders/feature-flags/index-engine"
|
||||
|
||||
// TODO: For now we keep the files in memory, as that's how they get passed to the workflows
|
||||
// This will need revisiting once we are closer to prod-ready v2, since with workflows and potentially
|
||||
// services on other machines using streams is not as simple as it used to be.
|
||||
const upload = multer({ storage: multer.memoryStorage() })
|
||||
|
||||
export const adminProductRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
@@ -104,6 +102,11 @@ export const adminProductRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
matcher: "/admin/products/import",
|
||||
middlewares: [upload.single("file")],
|
||||
},
|
||||
{
|
||||
method: ["POST"],
|
||||
matcher: "/admin/products/imports",
|
||||
middlewares: [validateAndTransformBody(AdminImportProducts)],
|
||||
},
|
||||
{
|
||||
method: ["POST"],
|
||||
matcher: "/admin/products/import/:transaction_id/confirm",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { BatchMethodRequest } from "@medusajs/framework/types"
|
||||
import { BatchMethodRequest, HttpTypes } from "@medusajs/framework/types"
|
||||
import { ProductStatus } from "@medusajs/framework/utils"
|
||||
import { z } from "zod"
|
||||
import { z, ZodType } from "zod"
|
||||
import {
|
||||
applyAndAndOrOperators,
|
||||
booleanString,
|
||||
@@ -341,3 +341,12 @@ export type AdminBatchVariantInventoryItemsType = BatchMethodRequest<
|
||||
AdminBatchUpdateVariantInventoryItemType,
|
||||
AdminBatchDeleteVariantInventoryItemType
|
||||
>
|
||||
|
||||
export const AdminImportProducts = z.object({
|
||||
file_key: z.string(),
|
||||
originalname: z.string(),
|
||||
extension: z.string(),
|
||||
size: z.number(),
|
||||
mime_type: z.string(),
|
||||
}) satisfies ZodType<HttpTypes.AdminImportProductsRequest>
|
||||
export type AdminImportProductsType = z.infer<typeof AdminImportProducts>
|
||||
@@ -1,8 +1,11 @@
|
||||
import multer from "multer"
|
||||
import { MiddlewareRoute } from "@medusajs/framework/http"
|
||||
import {
|
||||
MiddlewareRoute,
|
||||
validateAndTransformBody,
|
||||
} from "@medusajs/framework/http"
|
||||
import { validateAndTransformQuery } from "@medusajs/framework"
|
||||
import { retrieveUploadConfig } from "./query-config"
|
||||
import { AdminGetUploadParams } from "./validators"
|
||||
import { AdminGetUploadParams, AdminUploadPreSignedUrl } from "./validators"
|
||||
|
||||
// TODO: For now we keep the files in memory, as that's how they get passed to the workflows
|
||||
// This will need revisiting once we are closer to prod-ready v2, since with workflows and potentially
|
||||
@@ -31,4 +34,9 @@ export const adminUploadRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
matcher: "/admin/uploads/:id",
|
||||
middlewares: [],
|
||||
},
|
||||
{
|
||||
method: ["POST"],
|
||||
matcher: "/admin/uploads/presigned-urls",
|
||||
middlewares: [validateAndTransformBody(AdminUploadPreSignedUrl)],
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,49 @@
|
||||
import { ulid } from "ulid"
|
||||
import { MIMEType } from "util"
|
||||
import type {
|
||||
MedusaResponse,
|
||||
AuthenticatedMedusaRequest,
|
||||
} from "@medusajs/framework/http"
|
||||
import {
|
||||
Modules,
|
||||
MedusaError,
|
||||
MedusaErrorTypes,
|
||||
} from "@medusajs/framework/utils"
|
||||
import type { HttpTypes } from "@medusajs/framework/types"
|
||||
import type { AdminUploadPreSignedUrlType } from "../validators"
|
||||
|
||||
export const POST = async (
|
||||
req: AuthenticatedMedusaRequest<AdminUploadPreSignedUrlType>,
|
||||
res: MedusaResponse<HttpTypes.AdminUploadPreSignedUrlResponse>
|
||||
) => {
|
||||
const fileProvider = req.scope.resolve(Modules.FILE)
|
||||
let type: MIMEType
|
||||
|
||||
try {
|
||||
type = new MIMEType(req.validatedBody.mime_type)
|
||||
} catch {
|
||||
throw new MedusaError(
|
||||
MedusaErrorTypes.INVALID_DATA,
|
||||
`Invalid file type "${req.validatedBody.mime_type}"`,
|
||||
MedusaErrorTypes.INVALID_DATA
|
||||
)
|
||||
}
|
||||
|
||||
const extension = type.subtype
|
||||
const uniqueFilename = `${ulid()}.${extension}`
|
||||
|
||||
const response = await fileProvider.getUploadFileUrls({
|
||||
filename: uniqueFilename,
|
||||
mimeType: req.validatedBody.mime_type,
|
||||
access: req.validatedBody.access ?? "private",
|
||||
})
|
||||
|
||||
res.json({
|
||||
url: response.url,
|
||||
filename: response.key,
|
||||
mime_type: type.toString(),
|
||||
size: req.validatedBody.size,
|
||||
extension,
|
||||
originalname: req.validatedBody.originalname,
|
||||
})
|
||||
}
|
||||
@@ -1,5 +1,17 @@
|
||||
import { z, ZodType } from "zod"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { createSelectParams } from "../../utils/validators"
|
||||
import { z } from "zod"
|
||||
|
||||
export type AdminGetUploadParamsType = z.infer<typeof AdminGetUploadParams>
|
||||
export const AdminGetUploadParams = createSelectParams()
|
||||
|
||||
export const AdminUploadPreSignedUrl = z.object({
|
||||
originalname: z.string(),
|
||||
mime_type: z.string(),
|
||||
size: z.number(),
|
||||
access: z.enum(["public", "private"]).optional(),
|
||||
}) satisfies ZodType<HttpTypes.AdminUploadPreSignedUrlRequest>
|
||||
|
||||
export type AdminUploadPreSignedUrlType = z.infer<
|
||||
typeof AdminUploadPreSignedUrl
|
||||
>
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user