mirror of
https://github.com/mudita/mudita-center.git
synced 2025-12-23 22:28:03 -05:00
[CP-3525][M1] Set Up HTTP Server & CLI for MTP Development (#2373)
Co-authored-by: slawomir-werner <slawomir.werner@mudita.com>
This commit is contained in:
committed by
mkurczewski
parent
b0a00c07e5
commit
8f1d3d6247
2
.gitignore
vendored
2
.gitignore
vendored
@@ -66,3 +66,5 @@ apps/mudita-center/static/sql-wasm*
|
||||
|
||||
matomo-to-gsheet-*.json
|
||||
scripts/manage-test-files/file-manager-test-files/*
|
||||
|
||||
libs/app-mtp/**/*.js
|
||||
|
||||
BIN
apps/mudita-center/resources/MtpFileTransfer_boxed.exe
Normal file
BIN
apps/mudita-center/resources/MtpFileTransfer_boxed.exe
Normal file
Binary file not shown.
33
libs/app-mtp/.eslintrc.json
Normal file
33
libs/app-mtp/.eslintrc.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../.eslintrc.js"
|
||||
],
|
||||
"ignorePatterns": [
|
||||
"!**/*"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"*.ts",
|
||||
"*.tsx",
|
||||
"*.js",
|
||||
"*.jsx"
|
||||
],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"*.ts",
|
||||
"*.tsx"
|
||||
],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"*.js",
|
||||
"*.jsx"
|
||||
],
|
||||
"rules": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
33
libs/app-mtp/README.md
Normal file
33
libs/app-mtp/README.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# app-mtp
|
||||
|
||||
# app-mtp-cli
|
||||
|
||||
`app-mtp-cli` is a CLI tool for interacting with MTP devices, supporting actions like retrieving devices, storages, and
|
||||
uploading files.
|
||||
|
||||
### Example Commands
|
||||
|
||||
#### 1. Get Devices
|
||||
|
||||
```bash
|
||||
npm run app-mtp:cli \'{\"action\":\"GET_DEVICES\", \"data\":\"\"}\'
|
||||
```
|
||||
|
||||
#### 2. Get Device Storages
|
||||
|
||||
```bash
|
||||
npm run app-mtp:cli \'{\"action\":\"GET_DEVICE_STORAGES\", \"deviceId\":\"device123\"}\'
|
||||
```
|
||||
|
||||
#### 3. Upload File
|
||||
|
||||
```bash
|
||||
npm run app-mtp:cli \'{\"action\":\"UPLOAD_FILE\", \"deviceId\":\"device123\", \"storageId\":\"storage456\", \"destinationPath\":\"/path/to/destination\", \"sourcePath\":\"/path/to/source\"}\'
|
||||
```
|
||||
|
||||
#### 4. Get Upload File Progress
|
||||
|
||||
```bash
|
||||
npm run app-mtp:cli \'{\"action\":\"GET_UPLOAD_FILE_PROGRESS\", \"transactionId\":\"transaction123\"}\'
|
||||
```
|
||||
|
||||
11
libs/app-mtp/jest.config.ts
Normal file
11
libs/app-mtp/jest.config.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/* eslint-disable */
|
||||
export default {
|
||||
displayName: "app-mtp",
|
||||
preset: "../../jest.preset.js",
|
||||
testEnvironment: "node",
|
||||
transform: {
|
||||
"^.+\\.[tj]s$": ["ts-jest", { tsconfig: "<rootDir>/tsconfig.spec.json" }],
|
||||
},
|
||||
moduleFileExtensions: ["ts", "js", "html"],
|
||||
coverageDirectory: "../../coverage/libs/app-mtp",
|
||||
}
|
||||
50
libs/app-mtp/project.json
Normal file
50
libs/app-mtp/project.json
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "app-mtp",
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "libs/app-mtp/src",
|
||||
"projectType": "library",
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@nx/js:tsc",
|
||||
"outputs": [
|
||||
"{options.outputPath}"
|
||||
],
|
||||
"options": {
|
||||
"outputPath": "dist/libs/app-mtp",
|
||||
"main": "libs/app-mtp/src/index.ts",
|
||||
"tsConfig": "libs/app-mtp/tsconfig.lib.json",
|
||||
"assets": [
|
||||
"libs/app-mtp/*.md"
|
||||
]
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint",
|
||||
"outputs": [
|
||||
"{options.outputFile}"
|
||||
]
|
||||
},
|
||||
"test": {
|
||||
"executor": "@nx/jest:jest",
|
||||
"outputs": [
|
||||
"{workspaceRoot}/coverage/{projectRoot}"
|
||||
],
|
||||
"options": {
|
||||
"jestConfig": "libs/app-mtp/jest.config.ts"
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
"executor": "nx:run-commands",
|
||||
"options": {
|
||||
"command": "nodemon --watch './libs/app-mtp/src/**' --ext 'ts' --exec 'ts-node --project libs/app-mtp/tsconfig.lib.json libs/app-mtp/src/lib/app-examples/app-mtp-server.ts'"
|
||||
}
|
||||
},
|
||||
"cli": {
|
||||
"executor": "nx:run-commands",
|
||||
"options": {
|
||||
"command": "nodemon --watch './libs/app-mtp/src/**' --ext 'ts' --exec 'ts-node --project libs/app-mtp/tsconfig.lib.json libs/app-mtp/src/lib/app-examples/app-mtp-cli.ts'"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
6
libs/app-mtp/src/index.ts
Normal file
6
libs/app-mtp/src/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
export * from "./lib/app-mtp"
|
||||
104
libs/app-mtp/src/lib/app-examples/app-mtp-cli.ts
Normal file
104
libs/app-mtp/src/lib/app-examples/app-mtp-cli.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import yargs from "yargs"
|
||||
import { hideBin } from "yargs/helpers"
|
||||
import { AppMtp } from "../app-mtp"
|
||||
import { GetUploadFileProgress, MtpUploadFileData } from "../app-mtp.interface"
|
||||
import * as dotenv from "dotenv"
|
||||
|
||||
dotenv.config()
|
||||
const appMtp = new AppMtp()
|
||||
|
||||
enum MtpCliCommandAction {
|
||||
GET_DEVICES = "GET_DEVICES",
|
||||
GET_DEVICE_STORAGES = "GET_DEVICE_STORAGES",
|
||||
UPLOAD_FILE = "UPLOAD_FILE",
|
||||
GET_UPLOAD_FILE_PROGRESS = "GET_UPLOAD_FILE_PROGRESS",
|
||||
}
|
||||
|
||||
interface MtpCliCommand {
|
||||
action: MtpCliCommandAction
|
||||
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
const handleAction = (action: MtpCliCommandAction, parsedData: unknown) => {
|
||||
switch (action) {
|
||||
case MtpCliCommandAction.GET_DEVICES:
|
||||
appMtp
|
||||
.getDevices()
|
||||
.then((devices) => {
|
||||
console.log("[app-mtp-cli] output: ", devices)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("[app-mtp-cli] output: Error fetching devices:", err)
|
||||
})
|
||||
break
|
||||
|
||||
case MtpCliCommandAction.GET_DEVICE_STORAGES:
|
||||
appMtp
|
||||
.getDeviceStorages((parsedData as { deviceId: string }).deviceId)
|
||||
.then((storages) => {
|
||||
console.log("[app-mtp-cli] output:", storages)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(
|
||||
"[app-mtp-cli] output: Error fetching device storages:",
|
||||
err
|
||||
)
|
||||
})
|
||||
break
|
||||
|
||||
case MtpCliCommandAction.UPLOAD_FILE:
|
||||
appMtp
|
||||
.uploadFile(parsedData as MtpUploadFileData)
|
||||
.then(() => {
|
||||
console.log("[app-mtp-cli] output: File uploaded successfully.")
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("[app-mtp-cli] output: Error uploading file:", err)
|
||||
})
|
||||
break
|
||||
|
||||
case MtpCliCommandAction.GET_UPLOAD_FILE_PROGRESS:
|
||||
appMtp
|
||||
.getUploadFileProgress(parsedData as GetUploadFileProgress)
|
||||
.then((progress) => {
|
||||
console.log("[app-mtp-cli] output:", progress)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(
|
||||
"[app-mtp-cli] output: Error fetching upload file progress:",
|
||||
err
|
||||
)
|
||||
})
|
||||
break
|
||||
|
||||
default:
|
||||
console.error("[app-mtp-cli] output: Unknown action:", action)
|
||||
}
|
||||
}
|
||||
|
||||
yargs(hideBin(process.argv))
|
||||
.middleware((argv) => {
|
||||
const jsonString = argv._[0] as string
|
||||
if (jsonString) {
|
||||
try {
|
||||
const parsedData: MtpCliCommand = JSON.parse(jsonString)
|
||||
console.log("[app-mtp-cli] input: ", parsedData)
|
||||
|
||||
const { action } = parsedData
|
||||
|
||||
handleAction(action, parsedData)
|
||||
} catch (error) {
|
||||
console.error("[app-mtp-cli] output: Invalid JSON string:", jsonString)
|
||||
console.error("[app-mtp-cli] output:", error)
|
||||
}
|
||||
} else {
|
||||
console.error("[app-mtp-cli] output: No JSON string provided.")
|
||||
}
|
||||
})
|
||||
.help().argv
|
||||
50
libs/app-mtp/src/lib/app-examples/app-mtp-server.ts
Normal file
50
libs/app-mtp/src/lib/app-examples/app-mtp-server.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import express, { Application, Request, Response, Router } from "express"
|
||||
import bodyParser from "body-parser"
|
||||
import http from "http"
|
||||
import { AppMtp } from "../app-mtp"
|
||||
import * as dotenv from "dotenv"
|
||||
|
||||
dotenv.config()
|
||||
const appMtp = new AppMtp()
|
||||
|
||||
const app: Application = express()
|
||||
const router: Router = Router()
|
||||
|
||||
app.use(bodyParser.json())
|
||||
|
||||
router.get("/get-devices", (req: Request, res: Response) => {
|
||||
appMtp.getDevices().then((devices) => {
|
||||
res.json(devices)
|
||||
})
|
||||
})
|
||||
|
||||
router.get("/get-device-storages/:deviceId", (req: Request, res: Response) => {
|
||||
const { deviceId } = req.params
|
||||
appMtp.getDeviceStorages(deviceId).then((storages) => {
|
||||
res.json(storages)
|
||||
})
|
||||
})
|
||||
|
||||
router.post("/upload-file", (req: Request, res: Response) => {
|
||||
appMtp.uploadFile(req.body).then((result) => {
|
||||
res.json(result)
|
||||
})
|
||||
})
|
||||
|
||||
router.get("/upload-file-progress", (req: Request, res: Response) => {
|
||||
appMtp.getUploadFileProgress(req.body).then((result) => {
|
||||
res.json(result)
|
||||
})
|
||||
})
|
||||
|
||||
app.use("/", router)
|
||||
|
||||
const server = http.createServer(app)
|
||||
server.listen(3000, () => {
|
||||
console.log("[app-mtp-server] running at http://127.0.0.1:3000/")
|
||||
})
|
||||
19
libs/app-mtp/src/lib/app-mtp.factory.ts
Normal file
19
libs/app-mtp/src/lib/app-mtp.factory.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import { MtpInterface } from "./app-mtp.interface"
|
||||
import { DotnetMtp } from "./dotnet-mtp/dotnet-mtp"
|
||||
import { NodeMtp } from "./node-mtp/node-mtp"
|
||||
import { NodeMtpDeviceManager } from "./node-mtp/node-mtp-device-manager"
|
||||
|
||||
export class MtpFactory {
|
||||
static createInstance(): MtpInterface {
|
||||
if (process.platform === "win32") {
|
||||
return new DotnetMtp()
|
||||
} else {
|
||||
return new NodeMtp(new NodeMtpDeviceManager())
|
||||
}
|
||||
}
|
||||
}
|
||||
63
libs/app-mtp/src/lib/app-mtp.interface.ts
Normal file
63
libs/app-mtp/src/lib/app-mtp.interface.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import { ResultObject } from "../../../core/core/builder/result.builder"
|
||||
import { AppError } from "../../../core/core/errors/app-error"
|
||||
|
||||
export interface MtpDevice {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface MtpStorage {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface TransactionStatus {
|
||||
progress: number
|
||||
error?: AppError
|
||||
}
|
||||
|
||||
export enum MTPError {
|
||||
MTP_DEVICE_NOT_FOUND = "MTP_DEVICE_NOT_FOUND",
|
||||
MTP_STORAGE_NOT_FOUND = "MTP_STORAGE_NOT_FOUND",
|
||||
MTP_SOURCE_PATH_NOT_FOUND = "MTP_SOURCE_PATH_NOT_FOUND",
|
||||
MTP_TRANSACTION_NOT_FOUND = "MTP_TRANSACTION_NOT_FOUND",
|
||||
MTP_GENERAL_ERROR = "MTP_GENERAL_ERROR",
|
||||
}
|
||||
|
||||
export interface MtpUploadFileData {
|
||||
deviceId: string
|
||||
storageId: string
|
||||
destinationPath: string
|
||||
sourcePath: string
|
||||
}
|
||||
|
||||
export interface UploadFileResultData {
|
||||
transactionId: string
|
||||
}
|
||||
|
||||
export interface GetUploadFileProgress {
|
||||
transactionId: string
|
||||
}
|
||||
|
||||
export interface GetUploadFileProgressResultData {
|
||||
progress: number
|
||||
}
|
||||
|
||||
export interface MtpInterface {
|
||||
getDevices(): Promise<MtpDevice[]>
|
||||
|
||||
getDeviceStorages(deviceId: string): Promise<ResultObject<MtpStorage[]>>
|
||||
|
||||
uploadFile(
|
||||
data: MtpUploadFileData
|
||||
): Promise<ResultObject<UploadFileResultData>>
|
||||
|
||||
getUploadFileProgress(
|
||||
data: GetUploadFileProgress
|
||||
): Promise<ResultObject<GetUploadFileProgressResultData>>
|
||||
}
|
||||
67
libs/app-mtp/src/lib/app-mtp.ts
Normal file
67
libs/app-mtp/src/lib/app-mtp.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import {
|
||||
GetUploadFileProgress,
|
||||
GetUploadFileProgressResultData,
|
||||
MtpDevice,
|
||||
MtpInterface,
|
||||
MtpStorage,
|
||||
MtpUploadFileData,
|
||||
UploadFileResultData,
|
||||
} from "./app-mtp.interface"
|
||||
import { MtpFactory } from "./app-mtp.factory"
|
||||
import { ResultObject } from "../../../core/core/builder/result.builder"
|
||||
|
||||
export class AppMtp implements MtpInterface {
|
||||
private mtp: MtpInterface
|
||||
|
||||
constructor() {
|
||||
this.mtp = MtpFactory.createInstance()
|
||||
}
|
||||
|
||||
async getDevices(): Promise<MtpDevice[]> {
|
||||
console.log(`[app-mtp] getting devices`)
|
||||
const result = await this.mtp.getDevices()
|
||||
console.log(`[app-mtp] getting devices result: ${JSON.stringify(result)}`)
|
||||
return result
|
||||
}
|
||||
|
||||
async getDeviceStorages(
|
||||
deviceId: string
|
||||
): Promise<ResultObject<MtpStorage[]>> {
|
||||
console.log(`[app-mtp] getting device storages for device: ${deviceId}`)
|
||||
const result = await this.mtp.getDeviceStorages(deviceId)
|
||||
console.log(
|
||||
`[app-mtp] getting device storages result: ${JSON.stringify(result)}`
|
||||
)
|
||||
return result
|
||||
}
|
||||
|
||||
async uploadFile(
|
||||
data: MtpUploadFileData
|
||||
): Promise<ResultObject<UploadFileResultData>> {
|
||||
const result = await this.mtp.uploadFile(data)
|
||||
console.log(
|
||||
`[app-mtp] starting upload file process: ${JSON.stringify(result)}`
|
||||
)
|
||||
return result
|
||||
}
|
||||
|
||||
async getUploadFileProgress(
|
||||
data: GetUploadFileProgress
|
||||
): Promise<ResultObject<GetUploadFileProgressResultData>> {
|
||||
console.log(
|
||||
`[app-mtp] getting upload file progress for transaction: ${data.transactionId}`
|
||||
)
|
||||
const result = await this.mtp.getUploadFileProgress(data)
|
||||
|
||||
console.log(
|
||||
`[app-mtp] getting upload file progress result: ${JSON.stringify(result)}`
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
97
libs/app-mtp/src/lib/dotnet-mtp/dotnet-mtp.ts
Normal file
97
libs/app-mtp/src/lib/dotnet-mtp/dotnet-mtp.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import { spawn } from "child_process"
|
||||
import path from "path"
|
||||
import {
|
||||
GetUploadFileProgress,
|
||||
GetUploadFileProgressResultData,
|
||||
MtpDevice,
|
||||
MtpInterface,
|
||||
MtpStorage,
|
||||
MtpUploadFileData,
|
||||
TransactionStatus,
|
||||
UploadFileResultData,
|
||||
} from "../app-mtp.interface"
|
||||
import { generateId } from "../utils/generate-id"
|
||||
import {
|
||||
Result,
|
||||
ResultObject,
|
||||
} from "../../../../core/core/builder/result.builder"
|
||||
|
||||
export class DotnetMtp implements MtpInterface {
|
||||
private uploadFileTransactionStatus: Record<string, TransactionStatus> = {}
|
||||
|
||||
async getDevices(): Promise<MtpDevice[]> {
|
||||
return [{ id: "device-1", name: "Device 1" }]
|
||||
}
|
||||
|
||||
async getDeviceStorages(
|
||||
deviceId: string
|
||||
): Promise<ResultObject<MtpStorage[]>> {
|
||||
return Result.success([
|
||||
{ id: "storage-1", name: "Storage 1" },
|
||||
{ id: "storage-2", name: "Storage 2" },
|
||||
])
|
||||
}
|
||||
|
||||
async uploadFile(
|
||||
data: MtpUploadFileData
|
||||
): Promise<ResultObject<UploadFileResultData>> {
|
||||
const transactionId = generateId()
|
||||
void this.processFileUpload(data, transactionId)
|
||||
return Result.success({ transactionId })
|
||||
}
|
||||
|
||||
async getUploadFileProgress({
|
||||
transactionId,
|
||||
}: GetUploadFileProgress): Promise<
|
||||
ResultObject<GetUploadFileProgressResultData>
|
||||
> {
|
||||
return Result.success({
|
||||
progress: this.uploadFileTransactionStatus[transactionId].progress,
|
||||
})
|
||||
}
|
||||
|
||||
private async processFileUpload(
|
||||
data: MtpUploadFileData,
|
||||
transactionId: string
|
||||
): Promise<void> {
|
||||
this.uploadFileTransactionStatus[transactionId] = {
|
||||
progress: 0,
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const exePath = path.join(
|
||||
__dirname,
|
||||
"../../../../../apps/mudita-center/resources/MtpFileTransfer_boxed.exe"
|
||||
)
|
||||
const args = '{"action":"UPLOAD_FILE"}'
|
||||
const child = spawn(exePath, [args])
|
||||
|
||||
child.stdout.on("data", (data) => {
|
||||
console.log(`[app-mtp/dotnet-mtp] data stdout: ${data}`)
|
||||
this.uploadFileTransactionStatus[transactionId].progress =
|
||||
JSON.parse(data).data.progress
|
||||
})
|
||||
|
||||
child.stderr.on("data", (data) => {
|
||||
console.error(`[app-mtp/dotnet-mtp] data stderr: ${data}`)
|
||||
})
|
||||
|
||||
child.on("close", (code) => {
|
||||
if (code !== 0) {
|
||||
console.log(
|
||||
`[app-mtp/dotnet-mtp] child process exited with code: ${code}`
|
||||
)
|
||||
} else {
|
||||
console.log(`[app-mtp/dotnet-mtp] child process exited successfully`)
|
||||
}
|
||||
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
13
libs/app-mtp/src/lib/node-mtp/node-mtp-device-manager.ts
Normal file
13
libs/app-mtp/src/lib/node-mtp/node-mtp-device-manager.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import { NodeMtpDevice } from "./node-mtp-device"
|
||||
|
||||
export class NodeMtpDeviceManager {
|
||||
getDevice(): NodeMtpDevice {
|
||||
// mock implementation
|
||||
return new NodeMtpDevice()
|
||||
}
|
||||
}
|
||||
26
libs/app-mtp/src/lib/node-mtp/node-mtp-device.ts
Normal file
26
libs/app-mtp/src/lib/node-mtp/node-mtp-device.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
export interface UploadFileInfoOptions {
|
||||
size: number
|
||||
name: string
|
||||
storageId: number
|
||||
parentObjectHandle: number
|
||||
}
|
||||
|
||||
export class NodeMtpDevice {
|
||||
async uploadFileInfo(options: UploadFileInfoOptions): Promise<number> {
|
||||
// mock implementation
|
||||
return 0
|
||||
}
|
||||
|
||||
async uploadFileCommand() {
|
||||
// mock implementation
|
||||
}
|
||||
|
||||
async uploadFileData(chunk: Buffer | string) {
|
||||
// mock implementation
|
||||
}
|
||||
}
|
||||
183
libs/app-mtp/src/lib/node-mtp/node-mtp.ts
Normal file
183
libs/app-mtp/src/lib/node-mtp/node-mtp.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import path from "node:path"
|
||||
import fs from "node:fs"
|
||||
import { NodeMtpDeviceManager } from "./node-mtp-device-manager"
|
||||
import {
|
||||
GetUploadFileProgress,
|
||||
GetUploadFileProgressResultData,
|
||||
MtpDevice,
|
||||
MTPError,
|
||||
MtpInterface,
|
||||
MtpStorage,
|
||||
MtpUploadFileData,
|
||||
TransactionStatus,
|
||||
UploadFileResultData,
|
||||
} from "../app-mtp.interface"
|
||||
import { generateId } from "../utils/generate-id"
|
||||
import { isEmpty } from "../utils/is-empty"
|
||||
import {
|
||||
Result,
|
||||
ResultObject,
|
||||
} from "../../../../core/core/builder/result.builder"
|
||||
import { AppError } from "../../../../core/core/errors/app-error"
|
||||
|
||||
export const delay = (ms: number = 500): Promise<void> => {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
export class NodeMtp implements MtpInterface {
|
||||
private uploadFileTransactionStatus: Record<string, TransactionStatus> = {}
|
||||
|
||||
constructor(private deviceManager: NodeMtpDeviceManager) {}
|
||||
|
||||
async getDevices(): Promise<MtpDevice[]> {
|
||||
return [{ id: "device-1", name: "Device 1" }]
|
||||
}
|
||||
|
||||
async getDeviceStorages(
|
||||
deviceId: string
|
||||
): Promise<ResultObject<MtpStorage[]>> {
|
||||
if (isEmpty(deviceId)) {
|
||||
return Result.failed({ type: MTPError.MTP_DEVICE_NOT_FOUND } as AppError)
|
||||
}
|
||||
|
||||
return Result.success([
|
||||
{ id: "storage-1", name: "Storage 1" },
|
||||
{ id: "storage-2", name: "Storage 2" },
|
||||
])
|
||||
}
|
||||
|
||||
async uploadFile(
|
||||
data: MtpUploadFileData
|
||||
): Promise<ResultObject<UploadFileResultData>> {
|
||||
if (isEmpty(data.deviceId)) {
|
||||
return Result.failed({ type: MTPError.MTP_DEVICE_NOT_FOUND } as AppError)
|
||||
}
|
||||
|
||||
const result = await this.processUploadFileInfo(data)
|
||||
|
||||
if (!result.ok) {
|
||||
return result
|
||||
}
|
||||
|
||||
const transactionId = generateId()
|
||||
void this.processUploadFile(data, transactionId)
|
||||
return Result.success({ transactionId })
|
||||
}
|
||||
|
||||
async getUploadFileProgress({
|
||||
transactionId,
|
||||
}: GetUploadFileProgress): Promise<
|
||||
ResultObject<GetUploadFileProgressResultData>
|
||||
> {
|
||||
if (isEmpty(this.uploadFileTransactionStatus[transactionId])) {
|
||||
return Result.failed({
|
||||
type: MTPError.MTP_TRANSACTION_NOT_FOUND,
|
||||
} as AppError)
|
||||
}
|
||||
|
||||
if (this.uploadFileTransactionStatus[transactionId].error) {
|
||||
return Result.failed(
|
||||
this.uploadFileTransactionStatus[transactionId].error as AppError
|
||||
)
|
||||
}
|
||||
|
||||
return Result.success({
|
||||
progress: this.uploadFileTransactionStatus[transactionId].progress,
|
||||
})
|
||||
}
|
||||
|
||||
private async processUploadFileInfo({
|
||||
sourcePath,
|
||||
destinationPath,
|
||||
}: MtpUploadFileData): Promise<ResultObject<number>> {
|
||||
try {
|
||||
if (!fs.existsSync(sourcePath)) {
|
||||
return Result.failed({
|
||||
type: MTPError.MTP_SOURCE_PATH_NOT_FOUND,
|
||||
message:
|
||||
"`sourcePath` not found. Please check the provided path in the request.",
|
||||
} as AppError)
|
||||
}
|
||||
|
||||
const PHONE_STORAGE_ID = 65537
|
||||
// const SD_CARD_STORAGE_ID = 131073
|
||||
|
||||
const device = this.deviceManager.getDevice()
|
||||
const size = await this.getFileSize(sourcePath)
|
||||
const name = path.basename(sourcePath)
|
||||
const parentObjectHandle = this.getParentObjectHandle(destinationPath)
|
||||
|
||||
const newObjectID = await device.uploadFileInfo({
|
||||
size,
|
||||
name,
|
||||
storageId: PHONE_STORAGE_ID,
|
||||
parentObjectHandle,
|
||||
})
|
||||
|
||||
if (newObjectID === undefined) {
|
||||
console.log(
|
||||
`[app-mtp/node-mtp] process upload file info error - newObjectID is undefined`
|
||||
)
|
||||
return Result.failed({ type: MTPError.MTP_GENERAL_ERROR } as AppError)
|
||||
}
|
||||
|
||||
return Result.success(newObjectID)
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
console.log(`[app-mtp/node-mtp] process upload file info error: ${error}`)
|
||||
return Result.failed({ type: MTPError.MTP_GENERAL_ERROR } as AppError)
|
||||
}
|
||||
}
|
||||
|
||||
private async processUploadFile(
|
||||
{ sourcePath, destinationPath }: MtpUploadFileData,
|
||||
transactionId: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
this.uploadFileTransactionStatus[transactionId] = {
|
||||
progress: 0,
|
||||
}
|
||||
|
||||
const device = this.deviceManager.getDevice()
|
||||
const size = await this.getFileSize(sourcePath)
|
||||
|
||||
const fileStream = fs.createReadStream(sourcePath, {
|
||||
highWaterMark: 1024,
|
||||
})
|
||||
let uploadedBytes = 0
|
||||
|
||||
for await (const chunk of fileStream) {
|
||||
await delay(200)
|
||||
await device.uploadFileData(chunk)
|
||||
|
||||
uploadedBytes += chunk.length
|
||||
const progress = (uploadedBytes / size) * 100
|
||||
this.uploadFileTransactionStatus[transactionId].progress = progress
|
||||
console.log(`[app-mtp/node-mtp] progress: ${progress}%`)
|
||||
}
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
console.log(`[app-mtp/node-mtp] process upload file error: ${error}`)
|
||||
this.uploadFileTransactionStatus[transactionId].error = {
|
||||
type: MTPError.MTP_GENERAL_ERROR,
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
message: `Error uploading file in progress: ${this.uploadFileTransactionStatus[transactionId].progress}% - ${error}`,
|
||||
} as AppError
|
||||
}
|
||||
}
|
||||
|
||||
private async getFileSize(filePath: string): Promise<number> {
|
||||
const stats = await fs.promises.stat(filePath)
|
||||
return stats.size
|
||||
}
|
||||
|
||||
private getParentObjectHandle(filePath: string): number {
|
||||
// mock implementation
|
||||
return 0
|
||||
}
|
||||
}
|
||||
10
libs/app-mtp/src/lib/utils/generate-id.ts
Normal file
10
libs/app-mtp/src/lib/utils/generate-id.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
export const generateId = (): string => {
|
||||
const timestamp = Date.now()
|
||||
const randomPart = Math.random().toString(36).slice(2, 11)
|
||||
return `${timestamp}${randomPart}`
|
||||
}
|
||||
18
libs/app-mtp/src/lib/utils/is-empty.ts
Normal file
18
libs/app-mtp/src/lib/utils/is-empty.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
/**
|
||||
* Helper function to check if a value is considered "empty."
|
||||
* This is a temporary helper until MTP is used as a mock.
|
||||
*/
|
||||
export const isEmpty = (value: unknown): boolean => {
|
||||
return (
|
||||
value === null ||
|
||||
value === undefined ||
|
||||
value === "" ||
|
||||
value === 0 ||
|
||||
value === "0"
|
||||
)
|
||||
}
|
||||
22
libs/app-mtp/tsconfig.json
Normal file
22
libs/app-mtp/tsconfig.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
],
|
||||
"extends": "../../tsconfig.base.json"
|
||||
}
|
||||
19
libs/app-mtp/tsconfig.lib.json
Normal file
19
libs/app-mtp/tsconfig.lib.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"declaration": true,
|
||||
"types": [
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"./libs/app-mtp/src/**/*.ts",
|
||||
"./libs/core/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"jest.config.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.test.ts"
|
||||
]
|
||||
}
|
||||
17
libs/app-mtp/tsconfig.spec.json
Normal file
17
libs/app-mtp/tsconfig.spec.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"module": "commonjs",
|
||||
"types": [
|
||||
"jest",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"jest.config.ts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
1312
package-lock.json
generated
1312
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -19,6 +19,8 @@
|
||||
"app:develop": "nx develop mudita-center",
|
||||
"app:dist": "nx dist mudita-center",
|
||||
"app:translations:sync": "nx translations:sync mudita-center",
|
||||
"app-mtp:cli": "nx run app-mtp:cli",
|
||||
"app-mtp:serve": "nx run app-mtp:serve",
|
||||
"test": "npm run test:nx && npm run test:core",
|
||||
"test:core": "cross-env TZ=UTC jest --config=jest/jest.config.js",
|
||||
"test:nx": "nx run-many --target=test --exclude api-devices-testing",
|
||||
@@ -197,6 +199,7 @@
|
||||
"is-electron-renderer": "^2.0.1",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jest-environment-node": "^29.4.1",
|
||||
"jest-styled-components": "^7.0.8",
|
||||
"lint-staged": "^13.0.3",
|
||||
"lodash": "^4.17.21",
|
||||
@@ -207,7 +210,7 @@
|
||||
"moment": "^2.29.4",
|
||||
"node-gyp": "^10.0.1",
|
||||
"node-polyfill-webpack-plugin": "^2.0.1",
|
||||
"nodemon": "^2.0.19",
|
||||
"nodemon": "^2.0.22",
|
||||
"nx": "^17.1.3",
|
||||
"p-queue": "^7.3.0",
|
||||
"prettier": "^2.7.1",
|
||||
@@ -254,7 +257,7 @@
|
||||
"tcp-port-used": "^1.0.2",
|
||||
"terser-webpack-plugin": "^5.3.6",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.2.2",
|
||||
"url-loader": "4.1.1",
|
||||
"usb": "^1.9.2",
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
"libs/active-device-registry/models/src/index.ts"
|
||||
],
|
||||
"api-devices-testing": ["libs/api-devices-testing/src/index.ts"],
|
||||
"app-mtp": ["libs/app-mtp/src/index.ts"],
|
||||
"core-device/feature": ["libs/core-device/feature/src/index.ts"],
|
||||
"core-device/models": ["libs/core-device/models/src/index.ts"],
|
||||
"device-manager/feature": ["libs/device-manager/feature/src/index.ts"],
|
||||
|
||||
Reference in New Issue
Block a user