feat(medusa-file-local): implement missing streaming methods (#4788)

This commit is contained in:
Frane Polić
2023-08-23 13:05:04 +02:00
committed by GitHub
parent 4843cc6301
commit d8a6e3e0d8
10 changed files with 189 additions and 107 deletions
+9
View File
@@ -0,0 +1,9 @@
---
"@medusajs/medusa": minor
"@medusajs/file-local": patch
"medusa-file-minio": patch
"medusa-file-s3": patch
"@medusajs/types": patch
---
feat(medusa-file-local): local file service streaming methods
@@ -1,11 +1,13 @@
import { AbstractFileService, IFileService } from "@medusajs/medusa"
import { import {
AbstractFileService,
FileServiceGetUploadStreamResult, FileServiceGetUploadStreamResult,
FileServiceUploadResult, FileServiceUploadResult,
IFileService, UploadStreamDescriptorType,
} from "@medusajs/medusa" } from "@medusajs/types"
import fs from "fs" import fs from "fs"
import { parse } from "path" import path from "path"
import stream from "stream"
class LocalService extends AbstractFileService implements IFileService { class LocalService extends AbstractFileService implements IFileService {
protected uploadDir_: string protected uploadDir_: string
@@ -14,7 +16,7 @@ class LocalService extends AbstractFileService implements IFileService {
constructor({}, options) { constructor({}, options) {
super({}, options) super({}, options)
this.uploadDir_ = options.upload_dir || "uploads/images" this.uploadDir_ = options.upload_dir || "uploads"
this.backendUrl_ = options.backend_url || "http://localhost:9000" this.backendUrl_ = options.backend_url || "http://localhost:9000"
} }
@@ -29,40 +31,91 @@ class LocalService extends AbstractFileService implements IFileService {
async uploadFile( async uploadFile(
file: Express.Multer.File, file: Express.Multer.File,
options = {} options = {}
): Promise<{ url: string }> { ): Promise<FileServiceUploadResult> {
const parsedFilename = parse(file.originalname) const parsedFilename = path.parse(file.originalname)
const fileKey = `${parsedFilename.name}-${Date.now()}${parsedFilename.ext}` if (parsedFilename.dir) {
this.ensureDirExists(parsedFilename.dir)
}
const fileKey = path.join(
parsedFilename.dir,
`${Date.now()}-${parsedFilename.base}`
)
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fs.copyFile(file.path, `${this.uploadDir_}/${fileKey}`, (err) => { fs.copyFile(file.path, `${this.uploadDir_}/${fileKey}`, (err) => {
if (err) { if (err) {
reject(err)
throw err throw err
} }
const fileUrl = `${this.backendUrl_}/${this.uploadDir_}/${fileKey}` const fileUrl = `${this.backendUrl_}/${this.uploadDir_}/${fileKey}`
resolve({ url: fileUrl }) resolve({ url: fileUrl, key: fileKey })
}) })
}) })
} }
async delete(file): Promise<void> { async delete(file): Promise<void> {
throw Error("Not implemented") const filePath = `${this.uploadDir_}/${file.fileKey}`
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath)
}
} }
async getUploadStreamDescriptor( async getUploadStreamDescriptor(
fileData fileData: UploadStreamDescriptorType
): Promise<FileServiceGetUploadStreamResult> { ): Promise<FileServiceGetUploadStreamResult> {
throw Error("Not implemented") const parsedFilename = path.parse(
fileData.name + (fileData.ext ? `.${fileData.ext}` : "")
)
if (parsedFilename.dir) {
this.ensureDirExists(parsedFilename.dir)
}
const fileKey = path.join(
parsedFilename.dir,
`${Date.now()}-${parsedFilename.base}`
)
const fileUrl = `${this.backendUrl_}/${this.uploadDir_}/${fileKey}`
const pass = new stream.PassThrough()
const writeStream = fs.createWriteStream(`${this.uploadDir_}/${fileKey}`)
pass.pipe(writeStream) // for consistency with the IFileService
const promise = new Promise((res, rej) => {
writeStream.on("finish", res)
writeStream.on("error", rej)
})
return { url: fileUrl, fileKey, writeStream: pass, promise }
} }
async getDownloadStream(fileData): Promise<NodeJS.ReadableStream> { async getDownloadStream(fileData): Promise<NodeJS.ReadableStream> {
throw Error("Not implemented") const filePath = `${this.uploadDir_}/${fileData.fileKey}`
return fs.createReadStream(filePath)
} }
async getPresignedDownloadUrl(fileData): Promise<string> { async getPresignedDownloadUrl(fileData): Promise<string> {
throw Error("Not implemented") return `${this.backendUrl_}/${this.uploadDir_}/${fileData.fileKey}`
}
/**
* Ensure `uploadDir_` has nested directories provided as file path
*
* @param dirPath - file path relative to the base directory
* @private
*/
private ensureDirExists(dirPath: string) {
const relativePath = path.join(this.uploadDir_, dirPath)
if (!fs.existsSync(relativePath)) {
fs.mkdirSync(relativePath, { recursive: true })
}
} }
} }
@@ -1,11 +1,10 @@
import { AbstractFileService, IFileService } from "@medusajs/medusa"
import { import {
AbstractFileService,
DeleteFileType, DeleteFileType,
FileServiceUploadResult, FileServiceUploadResult,
GetUploadedFileType, GetUploadedFileType,
IFileService,
UploadStreamDescriptorType, UploadStreamDescriptorType,
} from "@medusajs/medusa" } from "@medusajs/types"
import { ClientConfiguration, PutObjectRequest } from "aws-sdk/clients/s3" import { ClientConfiguration, PutObjectRequest } from "aws-sdk/clients/s3"
import { MedusaError } from "medusa-core-utils" import { MedusaError } from "medusa-core-utils"
+2 -3
View File
@@ -1,14 +1,13 @@
import fs from "fs" import fs from "fs"
import aws from "aws-sdk" import aws from "aws-sdk"
import { parse } from "path" import { parse } from "path"
import { AbstractFileService, IFileService } from "@medusajs/medusa"
import { import {
AbstractFileService,
DeleteFileType, DeleteFileType,
FileServiceUploadResult, FileServiceUploadResult,
GetUploadedFileType, GetUploadedFileType,
IFileService,
UploadStreamDescriptorType, UploadStreamDescriptorType,
} from "@medusajs/medusa" } from "@medusajs/types"
import stream from "stream" import stream from "stream"
import { PutObjectRequest } from "aws-sdk/clients/s3" import { PutObjectRequest } from "aws-sdk/clients/s3"
import { ClientConfiguration } from "aws-sdk/clients/s3" import { ClientConfiguration } from "aws-sdk/clients/s3"
+7 -31
View File
@@ -1,35 +1,11 @@
import stream from "stream"
import { TransactionBaseService } from "./transaction-base-service" import { TransactionBaseService } from "./transaction-base-service"
import {
export type FileServiceUploadResult = { DeleteFileType,
url: string FileServiceGetUploadStreamResult,
} FileServiceUploadResult,
GetUploadedFileType,
export type FileServiceGetUploadStreamResult = { UploadStreamDescriptorType,
writeStream: stream.PassThrough } from "@medusajs/types"
promise: Promise<any>
url: string
fileKey: string
[x: string]: unknown
}
export type GetUploadedFileType = {
fileKey: string
isPrivate?: boolean
[x: string]: unknown
}
export type DeleteFileType = {
fileKey: string
[x: string]: unknown
}
export type UploadStreamDescriptorType = {
name: string
ext?: string
isPrivate?: boolean
[x: string]: unknown
}
export interface IFileService extends TransactionBaseService { export interface IFileService extends TransactionBaseService {
/** /**
+2 -2
View File
@@ -1,12 +1,12 @@
import { MedusaError } from "medusa-core-utils" import { MedusaError } from "medusa-core-utils"
import { EntityManager } from "typeorm" import { EntityManager } from "typeorm"
import { AbstractFileService } from "../interfaces"
import { import {
AbstractFileService,
FileServiceGetUploadStreamResult, FileServiceGetUploadStreamResult,
FileServiceUploadResult, FileServiceUploadResult,
GetUploadedFileType, GetUploadedFileType,
UploadStreamDescriptorType, UploadStreamDescriptorType,
} from "../interfaces" } from "@medusajs/types"
class DefaultFileService extends AbstractFileService { class DefaultFileService extends AbstractFileService {
async upload( async upload(
@@ -21,6 +21,9 @@ import {
TBuiltPriceListImportLine, TBuiltPriceListImportLine,
TParsedPriceListImportRowData, TParsedPriceListImportRowData,
} from "./types" } from "./types"
import { BatchJob } from "../../../models"
import { TParsedProductImportRowData } from "../product/types"
import { PriceListPriceCreateInput } from "../../../types/price-list"
/* /*
* Default strategy class used for a batch import of products/variants. * Default strategy class used for a batch import of products/variants.
@@ -298,7 +301,7 @@ class PriceListImportStrategy extends AbstractBatchJobStrategy {
// Upload new prices for price list // Upload new prices for price list
const priceImportOperations = await this.downloadImportOpsFile( const priceImportOperations = await this.downloadImportOpsFile(
batchJobId, batchJob,
OperationType.PricesCreate OperationType.PricesCreate
) )
@@ -306,7 +309,7 @@ class PriceListImportStrategy extends AbstractBatchJobStrategy {
priceImportOperations.map(async (op) => { priceImportOperations.map(async (op) => {
await txPriceListService.addPrices( await txPriceListService.addPrices(
priceListId, priceListId,
op.prices.map((p) => { (op.prices as PriceListPriceCreateInput[]).map((p) => {
return { return {
...p, ...p,
variant_id: op.variant_id, variant_id: op.variant_id,
@@ -328,14 +331,16 @@ class PriceListImportStrategy extends AbstractBatchJobStrategy {
*/ */
protected async uploadImportOpsFile( protected async uploadImportOpsFile(
batchJobId: string, batchJobId: string,
results: Record<OperationType, PriceListImportOperation[]> results: Record<OperationType, TParsedProductImportRowData[]>
): Promise<void> { ): Promise<void> {
const uploadPromises: Promise<void>[] = [] const uploadPromises: Promise<void>[] = []
const transactionManager = this.transactionManager_ ?? this.manager_ const transactionManager = this.transactionManager_ ?? this.manager_
const files: Record<string, string> = {}
for (const op in results) { for (const op in results) {
if (results[op]?.length) { if (results[op]?.length) {
const { writeStream, promise } = await this.fileService_ const { writeStream, fileKey, promise } = await this.fileService_
.withTransaction(transactionManager) .withTransaction(transactionManager)
.getUploadStreamDescriptor({ .getUploadStreamDescriptor({
name: PriceListImportStrategy.buildFilename(batchJobId, op), name: PriceListImportStrategy.buildFilename(batchJobId, op),
@@ -344,33 +349,38 @@ class PriceListImportStrategy extends AbstractBatchJobStrategy {
uploadPromises.push(promise) uploadPromises.push(promise)
files[op] = fileKey
writeStream.write(JSON.stringify(results[op])) writeStream.write(JSON.stringify(results[op]))
writeStream.end() writeStream.end()
} }
} }
await this.batchJobService_
.withTransaction(transactionManager)
.update(batchJobId, {
result: { files },
})
await Promise.all(uploadPromises) await Promise.all(uploadPromises)
} }
/** /**
* Remove parsed ops JSON file. * Download parsed ops JSON file.
* *
* @param batchJobId - An id of the current batch job being processed. * @param batchJob - the current batch job being processed
* @param op - Type of import operation. * @param op - Type of import operation.
*/ */
protected async downloadImportOpsFile( protected async downloadImportOpsFile(
batchJobId: string, batchJob: BatchJob,
op: OperationType op: OperationType
): Promise<PriceListImportOperation[]> { ): Promise<TParsedProductImportRowData[]> {
let data = "" let data = ""
const transactionManager = this.transactionManager_ ?? this.manager_ const transactionManager = this.transactionManager_ ?? this.manager_
const readableStream = await this.fileService_ const readableStream = await this.fileService_
.withTransaction(transactionManager) .withTransaction(transactionManager)
.getDownloadStream({ .getDownloadStream({
fileKey: PriceListImportStrategy.buildFilename(batchJobId, op, { fileKey: batchJob.result.files![op],
appendExt: ".json",
}),
}) })
return await new Promise((resolve) => { return await new Promise((resolve) => {
@@ -382,7 +392,7 @@ class PriceListImportStrategy extends AbstractBatchJobStrategy {
}) })
readableStream.on("error", () => { readableStream.on("error", () => {
// TODO: maybe should throw // TODO: maybe should throw
resolve([] as PriceListImportOperation[]) resolve([] as TParsedProductImportRowData[])
}) })
}) })
} }
@@ -390,18 +400,16 @@ class PriceListImportStrategy extends AbstractBatchJobStrategy {
/** /**
* Delete parsed CSV ops files. * Delete parsed CSV ops files.
* *
* @param batchJobId - An id of the current batch job being processed. * @param batchJob - the current batch job being processed
*/ */
protected async deleteOpsFiles(batchJobId: string): Promise<void> { protected async deleteOpsFiles(batchJob: BatchJob): Promise<void> {
const transactionManager = this.transactionManager_ ?? this.manager_ const transactionManager = this.transactionManager_ ?? this.manager_
const fileServiceTx = this.fileService_.withTransaction(transactionManager) const fileServiceTx = this.fileService_.withTransaction(transactionManager)
for (const op of Object.values(OperationType)) { for (const fileName of Object.values(batchJob.result.files!)) {
try { try {
await fileServiceTx.delete({ await fileServiceTx.delete({
fileKey: PriceListImportStrategy.buildFilename(batchJobId, op, { fileKey: fileName,
appendExt: ".json",
}),
}) })
} catch (e) { } catch (e) {
// noop // noop
@@ -432,7 +440,7 @@ class PriceListImportStrategy extends AbstractBatchJobStrategy {
.withTransaction(transactionManager) .withTransaction(transactionManager)
.delete({ fileKey }) .delete({ fileKey })
await this.deleteOpsFiles(batchJob.id) await this.deleteOpsFiles(batchJob)
} }
private static buildFilename( private static buildFilename(
@@ -420,7 +420,7 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
const transactionManager = this.transactionManager_ ?? this.manager_ const transactionManager = this.transactionManager_ ?? this.manager_
const productOps = await this.downloadImportOpsFile( const productOps = await this.downloadImportOpsFile(
batchJob.id, batchJob,
OperationType.ProductCreate OperationType.ProductCreate
) )
@@ -492,7 +492,7 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
const transactionManager = this.transactionManager_ ?? this.manager_ const transactionManager = this.transactionManager_ ?? this.manager_
const productOps = await this.downloadImportOpsFile( const productOps = await this.downloadImportOpsFile(
batchJob.id, batchJob,
OperationType.ProductUpdate OperationType.ProductUpdate
) )
@@ -568,7 +568,7 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
const transactionManager = this.transactionManager_ ?? this.manager_ const transactionManager = this.transactionManager_ ?? this.manager_
const variantOps = await this.downloadImportOpsFile( const variantOps = await this.downloadImportOpsFile(
batchJob.id, batchJob,
OperationType.VariantCreate OperationType.VariantCreate
) )
@@ -624,7 +624,7 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
const transactionManager = this.transactionManager_ ?? this.manager_ const transactionManager = this.transactionManager_ ?? this.manager_
const variantOps = await this.downloadImportOpsFile( const variantOps = await this.downloadImportOpsFile(
batchJob.id, batchJob,
OperationType.VariantUpdate OperationType.VariantUpdate
) )
@@ -691,9 +691,11 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
const uploadPromises: Promise<void>[] = [] const uploadPromises: Promise<void>[] = []
const transactionManager = this.transactionManager_ ?? this.manager_ const transactionManager = this.transactionManager_ ?? this.manager_
const files: Record<string, string> = {}
for (const op in results) { for (const op in results) {
if (results[op]?.length) { if (results[op]?.length) {
const { writeStream, promise } = await this.fileService_ const { writeStream, fileKey, promise } = await this.fileService_
.withTransaction(transactionManager) .withTransaction(transactionManager)
.getUploadStreamDescriptor({ .getUploadStreamDescriptor({
name: ProductImportStrategy.buildFilename(batchJobId, op), name: ProductImportStrategy.buildFilename(batchJobId, op),
@@ -702,22 +704,29 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
uploadPromises.push(promise) uploadPromises.push(promise)
files[op] = fileKey
writeStream.write(JSON.stringify(results[op])) writeStream.write(JSON.stringify(results[op]))
writeStream.end() writeStream.end()
} }
} }
await this.batchJobService_
.withTransaction(transactionManager)
.update(batchJobId, {
result: { files },
})
await Promise.all(uploadPromises) await Promise.all(uploadPromises)
} }
/** /**
* Remove parsed ops JSON file. * Download parsed ops JSON file.
* *
* @param batchJobId - An id of the current batch job being processed. * @param batchJob - the current batch job being processed
* @param op - Type of import operation. * @param op - Type of import operation.
*/ */
protected async downloadImportOpsFile( protected async downloadImportOpsFile(
batchJobId: string, batchJob: BatchJob,
op: OperationType op: OperationType
): Promise<TParsedProductImportRowData[]> { ): Promise<TParsedProductImportRowData[]> {
let data = "" let data = ""
@@ -726,9 +735,7 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
const readableStream = await this.fileService_ const readableStream = await this.fileService_
.withTransaction(transactionManager) .withTransaction(transactionManager)
.getDownloadStream({ .getDownloadStream({
fileKey: ProductImportStrategy.buildFilename(batchJobId, op, { fileKey: batchJob.result.files![op],
appendExt: ".json",
}),
}) })
return await new Promise((resolve) => { return await new Promise((resolve) => {
@@ -748,18 +755,16 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
/** /**
* Delete parsed CSV ops files. * Delete parsed CSV ops files.
* *
* @param batchJobId - An id of the current batch job being processed. * @param batchJob - the current batch job being processed
*/ */
protected async deleteOpsFiles(batchJobId: string): Promise<void> { protected async deleteOpsFiles(batchJob: BatchJob): Promise<void> {
const transactionManager = this.transactionManager_ ?? this.manager_ const transactionManager = this.transactionManager_ ?? this.manager_
const fileServiceTx = this.fileService_.withTransaction(transactionManager) const fileServiceTx = this.fileService_.withTransaction(transactionManager)
for (const op of Object.values(OperationType)) { for (const fileName of Object.values(batchJob.result.files!)) {
try { try {
await fileServiceTx.delete({ await fileServiceTx.delete({
fileKey: ProductImportStrategy.buildFilename(batchJobId, op, { fileKey: fileName,
appendExt: ".json",
}),
}) })
} catch (e) { } catch (e) {
// noop // noop
@@ -790,7 +795,7 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
.withTransaction(transactionManager) .withTransaction(transactionManager)
.delete({ fileKey }) .delete({ fileKey })
await this.deleteOpsFiles(batchJob.id) await this.deleteOpsFiles(batchJob)
} }
/** /**
+32
View File
@@ -0,0 +1,32 @@
import stream from "stream"
export type FileServiceUploadResult = {
url: string
key: string
}
export type FileServiceGetUploadStreamResult = {
writeStream: stream.PassThrough
promise: Promise<any>
url: string
fileKey: string
[x: string]: unknown
}
export type GetUploadedFileType = {
fileKey: string
isPrivate?: boolean
[x: string]: unknown
}
export type DeleteFileType = {
fileKey: string
[x: string]: unknown
}
export type UploadStreamDescriptorType = {
name: string
ext?: string
isPrivate?: boolean
[x: string]: unknown
}
+1
View File
@@ -6,6 +6,7 @@ export * from "./common"
export * from "./dal" export * from "./dal"
export * from "./event-bus" export * from "./event-bus"
export * from "./feature-flag" export * from "./feature-flag"
export * from "./file-service"
export * from "./inventory" export * from "./inventory"
export * from "./joiner" export * from "./joiner"
export * from "./logger" export * from "./logger"