mirror of
https://github.com/mudita/mudita-center.git
synced 2025-12-23 22:28:03 -05:00
[CP-3646] export on mac linux (#2583)
This commit is contained in:
@@ -253,6 +253,60 @@ export class NodeMtpDevice {
|
||||
await this.transferOut(new Uint8Array(chunk).buffer)
|
||||
}
|
||||
|
||||
public async initiateExportFile(
|
||||
sourcePath: string,
|
||||
storageId: number
|
||||
): Promise<{
|
||||
objectHandle: number
|
||||
fileName: string
|
||||
fileSize: number
|
||||
}> {
|
||||
const objectHandle = await this.findObjectHandleFromPath(
|
||||
storageId,
|
||||
sourcePath
|
||||
)
|
||||
const objectInfo = await this.getObjectInfo(objectHandle)
|
||||
return {
|
||||
objectHandle,
|
||||
fileName: objectInfo.filename,
|
||||
fileSize: objectInfo.objectCompressedSize,
|
||||
}
|
||||
}
|
||||
|
||||
public async exportFileData(
|
||||
objectHandle: number,
|
||||
offset: number,
|
||||
length: number
|
||||
): Promise<Uint8Array> {
|
||||
const transactionId = this.getTransactionId()
|
||||
|
||||
await this.write({
|
||||
transactionId,
|
||||
type: ContainerTypeCode.Command,
|
||||
code: ContainerCode.GetPartialObject,
|
||||
payload: [
|
||||
{ type: "UINT32", value: objectHandle },
|
||||
{ type: "UINT32", value: offset },
|
||||
{ type: "UINT32", value: length },
|
||||
],
|
||||
})
|
||||
|
||||
const dataResponse = await this.read(transactionId, ContainerTypeCode.Data)
|
||||
const statusResponse = await this.read(
|
||||
transactionId,
|
||||
ContainerTypeCode.Response
|
||||
)
|
||||
|
||||
if (statusResponse.code !== ContainerCode.StatusOk) {
|
||||
throw new AppError(
|
||||
MTPError.MTP_GENERAL_ERROR,
|
||||
`GetPartialObject failed at offset ${offset} with code: ${statusResponse.code}`
|
||||
)
|
||||
}
|
||||
|
||||
return new Uint8Array(dataResponse.payload)
|
||||
}
|
||||
|
||||
async cancelTransaction(): Promise<void> {
|
||||
if (!this.uploadTransactionId) {
|
||||
console.log(
|
||||
@@ -285,6 +339,18 @@ export class NodeMtpDevice {
|
||||
}
|
||||
}
|
||||
|
||||
public async getFileSize(
|
||||
sourcePath: string,
|
||||
storageId: number
|
||||
): Promise<number> {
|
||||
const objectHandle = await this.findObjectHandleFromPath(
|
||||
storageId,
|
||||
sourcePath
|
||||
)
|
||||
const objectInfo = await this.getObjectInfo(objectHandle)
|
||||
return objectInfo.objectCompressedSize
|
||||
}
|
||||
|
||||
private async transferOut(
|
||||
buffer: ArrayBuffer,
|
||||
timeoutMs: number = 5000,
|
||||
@@ -456,6 +522,36 @@ export class NodeMtpDevice {
|
||||
}
|
||||
}
|
||||
|
||||
private async findObjectHandleFromPath(storageId: number, fullPath: string) {
|
||||
const parts = fullPath.split("/").filter(Boolean)
|
||||
let currentHandle = 0xffffffff
|
||||
for (const part of parts) {
|
||||
const children = await this.getObjectHandles(
|
||||
currentHandle,
|
||||
storageId,
|
||||
undefined
|
||||
)
|
||||
let found = false
|
||||
for (const handle of children) {
|
||||
const info = await this.getObjectInfo(handle)
|
||||
if (info.filename === part) {
|
||||
currentHandle = handle
|
||||
found = true
|
||||
console.log(
|
||||
`${PREFIX_LOG} current handle: ${handle} for path ${part}`
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!found)
|
||||
throw new AppError(
|
||||
MTPError.MTP_SOURCE_PATH_NOT_FOUND,
|
||||
`There is no such element ${part} at ${fullPath} path`
|
||||
)
|
||||
}
|
||||
return currentHandle
|
||||
}
|
||||
|
||||
private getTransactionId(): number {
|
||||
const id = this.transactionIdCounter++
|
||||
if (this.transactionIdCounter >= 0xffffffff) {
|
||||
|
||||
@@ -34,7 +34,7 @@ import { ObjectFormatCode } from "./utils/object-format.interface"
|
||||
const PREFIX_LOG = `[app-mtp/node-mtp]`
|
||||
|
||||
export class NodeMtp implements MtpInterface {
|
||||
private uploadFileTransactionStatus: Record<string, TransactionStatus> = {}
|
||||
private transferFileTransactionStatus: Record<string, TransactionStatus> = {}
|
||||
private abortController: AbortController | undefined
|
||||
|
||||
constructor(private deviceManager: NodeMtpDeviceManager) {}
|
||||
@@ -93,9 +93,11 @@ export class NodeMtp implements MtpInterface {
|
||||
async exportFile(
|
||||
data: MtpTransferFileData
|
||||
): Promise<ResultObject<TransferFileResultData>> {
|
||||
return Result.failed(
|
||||
new AppError(MTPError.MTP_GENERAL_ERROR, "Not implemented yet!")
|
||||
)
|
||||
const transactionId = generateId()
|
||||
|
||||
void this.processExportFile(data, transactionId)
|
||||
|
||||
return Result.success({ transactionId })
|
||||
}
|
||||
|
||||
async getTransferredFileProgress({
|
||||
@@ -103,18 +105,18 @@ export class NodeMtp implements MtpInterface {
|
||||
}: TransferTransactionData): Promise<
|
||||
ResultObject<GetTransferFileProgressResultData>
|
||||
> {
|
||||
if (this.uploadFileTransactionStatus[transactionId] === undefined) {
|
||||
if (this.transferFileTransactionStatus[transactionId] === undefined) {
|
||||
return Result.failed(new AppError(MTPError.MTP_TRANSACTION_NOT_FOUND))
|
||||
}
|
||||
|
||||
if (this.uploadFileTransactionStatus[transactionId].error) {
|
||||
if (this.transferFileTransactionStatus[transactionId].error) {
|
||||
return Result.failed(
|
||||
this.uploadFileTransactionStatus[transactionId].error as AppError
|
||||
this.transferFileTransactionStatus[transactionId].error as AppError
|
||||
)
|
||||
}
|
||||
|
||||
return Result.success({
|
||||
progress: this.uploadFileTransactionStatus[transactionId].progress,
|
||||
progress: this.transferFileTransactionStatus[transactionId].progress,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -122,7 +124,7 @@ export class NodeMtp implements MtpInterface {
|
||||
data: TransferTransactionData
|
||||
): Promise<ResultObject<CancelTransferResultData>> {
|
||||
const transactionStatus =
|
||||
this.uploadFileTransactionStatus[data.transactionId]
|
||||
this.transferFileTransactionStatus[data.transactionId]
|
||||
|
||||
if (transactionStatus === undefined) {
|
||||
return Result.failed({
|
||||
@@ -236,7 +238,7 @@ export class NodeMtp implements MtpInterface {
|
||||
try {
|
||||
this.abortController = new AbortController()
|
||||
const startTime = Date.now()
|
||||
this.uploadFileTransactionStatus[transactionId] = {
|
||||
this.transferFileTransactionStatus[transactionId] = {
|
||||
progress: 0,
|
||||
}
|
||||
const device = await this.deviceManager.getNodeMtpDevice({ id: deviceId })
|
||||
@@ -250,17 +252,18 @@ export class NodeMtp implements MtpInterface {
|
||||
for await (const chunk of fileStream) {
|
||||
if (this.abortController.signal.aborted) {
|
||||
await device.cancelTransaction()
|
||||
this.uploadFileTransactionStatus[transactionId].error = new AppError(
|
||||
MTPError.MTP_PROCESS_CANCELLED,
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
`Error uploading file in progress: ${this.uploadFileTransactionStatus[transactionId].progress}% - ${MTPError.MTP_PROCESS_CANCELLED}`
|
||||
)
|
||||
this.transferFileTransactionStatus[transactionId].error =
|
||||
new AppError(
|
||||
MTPError.MTP_PROCESS_CANCELLED,
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
`Error uploading file in progress: ${this.transferFileTransactionStatus[transactionId].progress}% - ${MTPError.MTP_PROCESS_CANCELLED}`
|
||||
)
|
||||
return
|
||||
}
|
||||
await device.uploadFileData(chunk)
|
||||
uploadedBytes += chunk.length
|
||||
const progress = (uploadedBytes / size) * 100
|
||||
this.uploadFileTransactionStatus[transactionId].progress = progress
|
||||
this.transferFileTransactionStatus[transactionId].progress = progress
|
||||
console.log(`${PREFIX_LOG} progress: ${progress}%`)
|
||||
}
|
||||
|
||||
@@ -279,13 +282,85 @@ export class NodeMtp implements MtpInterface {
|
||||
console.log(`${PREFIX_LOG} process upload file error: ${error}`)
|
||||
|
||||
const mtpError = mapToMtpError(error)
|
||||
this.uploadFileTransactionStatus[transactionId].error =
|
||||
this.transferFileTransactionStatus[transactionId].error =
|
||||
mtpError.type === MTPError.MTP_INITIALIZE_ACCESS_ERROR
|
||||
? mtpError
|
||||
: new AppError(
|
||||
MTPError.MTP_GENERAL_ERROR,
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
`Error uploading file in progress: ${this.uploadFileTransactionStatus[transactionId].progress}% - ${error}`
|
||||
`Error uploading file at progress: ${this.transferFileTransactionStatus[transactionId].progress}% - ${error}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private async processExportFile(
|
||||
{ sourcePath, deviceId, storageId, destinationPath }: MtpTransferFileData,
|
||||
transactionId: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
this.abortController = new AbortController()
|
||||
const startTime = Date.now()
|
||||
this.transferFileTransactionStatus[transactionId] = { progress: 0 }
|
||||
|
||||
const device = await this.deviceManager.getNodeMtpDevice({ id: deviceId })
|
||||
const { objectHandle, fileName, fileSize } =
|
||||
await device.initiateExportFile(sourcePath, parseInt(storageId))
|
||||
|
||||
const outputPath = path.join(destinationPath, fileName)
|
||||
const writeStream = fs.createWriteStream(outputPath)
|
||||
|
||||
let offset = 0
|
||||
let downloadedBytes = 0
|
||||
|
||||
while (offset < fileSize) {
|
||||
if (this.abortController.signal.aborted) {
|
||||
writeStream.close()
|
||||
fs.rmSync(outputPath)
|
||||
this.transferFileTransactionStatus[transactionId].error =
|
||||
new AppError(
|
||||
MTPError.MTP_PROCESS_CANCELLED,
|
||||
`Export aborted at ${this.transferFileTransactionStatus[
|
||||
transactionId
|
||||
].progress.toFixed(2)}%`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const chunkSize = Math.min(mtpUploadChunkSize, fileSize - offset)
|
||||
const chunk = await device.exportFileData(
|
||||
objectHandle,
|
||||
offset,
|
||||
chunkSize
|
||||
)
|
||||
|
||||
writeStream.write(chunk)
|
||||
downloadedBytes += chunk.length
|
||||
offset += chunk.length
|
||||
|
||||
const progress = (downloadedBytes / fileSize) * 100
|
||||
this.transferFileTransactionStatus[transactionId].progress = progress
|
||||
console.log(`${PREFIX_LOG} export progress: ${progress.toFixed(2)}%`)
|
||||
}
|
||||
|
||||
writeStream.close()
|
||||
const duration = (Date.now() - startTime) / 1000
|
||||
const speed = fileSize / 1024 / 1024 / duration
|
||||
|
||||
console.log(
|
||||
`${PREFIX_LOG} File export completed in ${duration.toFixed(2)} seconds.`
|
||||
)
|
||||
console.log(`${PREFIX_LOG} Export speed: ${speed.toFixed(2)} MB/s`)
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
console.log(`${PREFIX_LOG} export file error: ${error}`)
|
||||
const mtpError = mapToMtpError(error)
|
||||
this.transferFileTransactionStatus[transactionId].error =
|
||||
mtpError.type === MTPError.MTP_INITIALIZE_ACCESS_ERROR
|
||||
? mtpError
|
||||
: new AppError(
|
||||
MTPError.MTP_GENERAL_ERROR,
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
`Error exporting file at progress: ${this.transferFileTransactionStatus[transactionId].progress}% - ${error}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,6 +293,10 @@ export const sendFiles = createAsyncThunk<
|
||||
return
|
||||
}
|
||||
|
||||
if (actionType == SendFilesAction.ActionExport) {
|
||||
return
|
||||
}
|
||||
|
||||
const entities = selectEntities(getState(), {
|
||||
deviceId,
|
||||
entitiesType,
|
||||
|
||||
Reference in New Issue
Block a user