feat(admin-service): 增强移动端版本上传功能
- 添加 APK/IPA 文件解析器自动提取版本信息 - 支持从安装包自动读取 versionName 和 versionCode - 添加 adbkit-apkreader 依赖解析 APK 文件 - 添加 plist 依赖解析 IPA 文件 - 优化上传接口支持自动填充版本信息 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
de5cbce0d3
commit
f9deca5df0
|
|
@ -19,12 +19,15 @@
|
||||||
"@nestjs/swagger": "^7.1.17",
|
"@nestjs/swagger": "^7.1.17",
|
||||||
"@prisma/client": "^5.7.0",
|
"@prisma/client": "^5.7.0",
|
||||||
"@types/multer": "^2.0.0",
|
"@types/multer": "^2.0.0",
|
||||||
|
"adbkit-apkreader": "^3.2.0",
|
||||||
|
"bplist-parser": "^0.3.2",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.0",
|
"class-validator": "^0.14.0",
|
||||||
"ioredis": "^5.3.2",
|
"ioredis": "^5.3.2",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rxjs": "^7.8.1"
|
"rxjs": "^7.8.1",
|
||||||
|
"unzipper": "^0.12.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^10.0.0",
|
"@nestjs/cli": "^10.0.0",
|
||||||
|
|
@ -35,6 +38,7 @@
|
||||||
"@types/node": "^20.3.1",
|
"@types/node": "^20.3.1",
|
||||||
"@types/passport-jwt": "^3.0.13",
|
"@types/passport-jwt": "^3.0.13",
|
||||||
"@types/supertest": "^6.0.0",
|
"@types/supertest": "^6.0.0",
|
||||||
|
"@types/unzipper": "^0.10.11",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||||
"@typescript-eslint/parser": "^6.0.0",
|
"@typescript-eslint/parser": "^6.0.0",
|
||||||
"eslint": "^8.42.0",
|
"eslint": "^8.42.0",
|
||||||
|
|
@ -2589,6 +2593,16 @@
|
||||||
"@types/superagent": "^8.1.0"
|
"@types/superagent": "^8.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/unzipper": {
|
||||||
|
"version": "0.10.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/unzipper/-/unzipper-0.10.11.tgz",
|
||||||
|
"integrity": "sha512-D25im2zjyMCcgL9ag6N46+wbtJBnXIr7SI4zHf9eJD2Dw2tEB5e+p5MYkrxKIVRscs5QV0EhtU9rgXSPx90oJg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/validator": {
|
"node_modules/@types/validator": {
|
||||||
"version": "13.15.10",
|
"version": "13.15.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz",
|
||||||
|
|
@ -3056,6 +3070,30 @@
|
||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/adbkit-apkreader": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/adbkit-apkreader/-/adbkit-apkreader-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-QwsxPYCqWSmCAiW/A4gq0eytb4jtZc7WNbECIhLCRfGEB38oXzIV/YkTpkOTQFKSg3S4Svb6y///qOUH7UrWWw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"bluebird": "^3.4.7",
|
||||||
|
"debug": "~4.1.1",
|
||||||
|
"yauzl": "^2.7.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/adbkit-apkreader/node_modules/debug": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
|
||||||
|
"deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ajv": {
|
"node_modules/ajv": {
|
||||||
"version": "8.12.0",
|
"version": "8.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
|
||||||
|
|
@ -3416,6 +3454,15 @@
|
||||||
"baseline-browser-mapping": "dist/cli.js"
|
"baseline-browser-mapping": "dist/cli.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/big-integer": {
|
||||||
|
"version": "1.6.52",
|
||||||
|
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz",
|
||||||
|
"integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==",
|
||||||
|
"license": "Unlicense",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/binary-extensions": {
|
"node_modules/binary-extensions": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||||
|
|
@ -3441,6 +3488,12 @@
|
||||||
"readable-stream": "^3.4.0"
|
"readable-stream": "^3.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bluebird": {
|
||||||
|
"version": "3.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
|
||||||
|
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/body-parser": {
|
"node_modules/body-parser": {
|
||||||
"version": "1.20.3",
|
"version": "1.20.3",
|
||||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
|
||||||
|
|
@ -3480,6 +3533,18 @@
|
||||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/bplist-parser": {
|
||||||
|
"version": "0.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.2.tgz",
|
||||||
|
"integrity": "sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"big-integer": "1.6.x"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 5.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||||
|
|
@ -3586,6 +3651,15 @@
|
||||||
"ieee754": "^1.1.13"
|
"ieee754": "^1.1.13"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/buffer-crc32": {
|
||||||
|
"version": "0.2.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
|
||||||
|
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/buffer-equal-constant-time": {
|
"node_modules/buffer-equal-constant-time": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||||
|
|
@ -4089,7 +4163,6 @@
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/cors": {
|
"node_modules/cors": {
|
||||||
|
|
@ -4396,6 +4469,45 @@
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/duplexer2": {
|
||||||
|
"version": "0.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
|
||||||
|
"integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"readable-stream": "^2.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/duplexer2/node_modules/readable-stream": {
|
||||||
|
"version": "2.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"core-util-is": "~1.0.0",
|
||||||
|
"inherits": "~2.0.3",
|
||||||
|
"isarray": "~1.0.0",
|
||||||
|
"process-nextick-args": "~2.0.0",
|
||||||
|
"safe-buffer": "~5.1.1",
|
||||||
|
"string_decoder": "~1.1.1",
|
||||||
|
"util-deprecate": "~1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/duplexer2/node_modules/safe-buffer": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/duplexer2/node_modules/string_decoder": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "~5.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/eastasianwidth": {
|
"node_modules/eastasianwidth": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||||
|
|
@ -5062,6 +5174,15 @@
|
||||||
"bser": "2.1.1"
|
"bser": "2.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fd-slicer": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"pend": "~1.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fflate": {
|
"node_modules/fflate": {
|
||||||
"version": "0.8.2",
|
"version": "0.8.2",
|
||||||
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
|
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
|
||||||
|
|
@ -5574,7 +5695,6 @@
|
||||||
"version": "4.2.11",
|
"version": "4.2.11",
|
||||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/graphemer": {
|
"node_modules/graphemer": {
|
||||||
|
|
@ -6023,6 +6143,12 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/isarray": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/isexe": {
|
"node_modules/isexe": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||||
|
|
@ -6930,7 +7056,6 @@
|
||||||
"version": "6.2.0",
|
"version": "6.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
|
||||||
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
|
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"universalify": "^2.0.0"
|
"universalify": "^2.0.0"
|
||||||
|
|
@ -7494,7 +7619,6 @@
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
||||||
"integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==",
|
"integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/node-releases": {
|
"node_modules/node-releases": {
|
||||||
|
|
@ -7847,6 +7971,12 @@
|
||||||
"resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
|
||||||
"integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
|
"integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
|
||||||
},
|
},
|
||||||
|
"node_modules/pend": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
|
|
@ -8045,6 +8175,12 @@
|
||||||
"fsevents": "2.3.3"
|
"fsevents": "2.3.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/process-nextick-args": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/prompts": {
|
"node_modules/prompts": {
|
||||||
"version": "2.4.2",
|
"version": "2.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
|
||||||
|
|
@ -9700,7 +9836,6 @@
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
||||||
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 10.0.0"
|
"node": ">= 10.0.0"
|
||||||
|
|
@ -9715,6 +9850,33 @@
|
||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/unzipper": {
|
||||||
|
"version": "0.12.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.12.3.tgz",
|
||||||
|
"integrity": "sha512-PZ8hTS+AqcGxsaQntl3IRBw65QrBI6lxzqDEL7IAo/XCEqRTKGfOX56Vea5TH9SZczRVxuzk1re04z/YjuYCJA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bluebird": "~3.7.2",
|
||||||
|
"duplexer2": "~0.1.4",
|
||||||
|
"fs-extra": "^11.2.0",
|
||||||
|
"graceful-fs": "^4.2.2",
|
||||||
|
"node-int64": "^0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/unzipper/node_modules/fs-extra": {
|
||||||
|
"version": "11.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz",
|
||||||
|
"integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"graceful-fs": "^4.2.0",
|
||||||
|
"jsonfile": "^6.0.1",
|
||||||
|
"universalify": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/update-browserslist-db": {
|
"node_modules/update-browserslist-db": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz",
|
||||||
|
|
@ -10125,6 +10287,16 @@
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/yauzl": {
|
||||||
|
"version": "2.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
|
||||||
|
"integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer-crc32": "~0.2.3",
|
||||||
|
"fd-slicer": "~1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yn": {
|
"node_modules/yn": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -39,12 +39,15 @@
|
||||||
"@nestjs/swagger": "^7.1.17",
|
"@nestjs/swagger": "^7.1.17",
|
||||||
"@prisma/client": "^5.7.0",
|
"@prisma/client": "^5.7.0",
|
||||||
"@types/multer": "^2.0.0",
|
"@types/multer": "^2.0.0",
|
||||||
|
"adbkit-apkreader": "^3.2.0",
|
||||||
|
"bplist-parser": "^0.3.2",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.0",
|
"class-validator": "^0.14.0",
|
||||||
"ioredis": "^5.3.2",
|
"ioredis": "^5.3.2",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rxjs": "^7.8.1"
|
"rxjs": "^7.8.1",
|
||||||
|
"unzipper": "^0.12.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^10.0.0",
|
"@nestjs/cli": "^10.0.0",
|
||||||
|
|
@ -55,6 +58,7 @@
|
||||||
"@types/node": "^20.3.1",
|
"@types/node": "^20.3.1",
|
||||||
"@types/passport-jwt": "^3.0.13",
|
"@types/passport-jwt": "^3.0.13",
|
||||||
"@types/supertest": "^6.0.0",
|
"@types/supertest": "^6.0.0",
|
||||||
|
"@types/unzipper": "^0.10.11",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||||
"@typescript-eslint/parser": "^6.0.0",
|
"@typescript-eslint/parser": "^6.0.0",
|
||||||
"eslint": "^8.42.0",
|
"eslint": "^8.42.0",
|
||||||
|
|
|
||||||
|
|
@ -103,14 +103,16 @@ export class VersionController {
|
||||||
@Get()
|
@Get()
|
||||||
@ApiOperation({ summary: '获取版本列表 (管理员)' })
|
@ApiOperation({ summary: '获取版本列表 (管理员)' })
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@ApiQuery({ name: 'platform', required: false, enum: Platform, description: '筛选平台' })
|
@ApiQuery({ name: 'platform', required: false, enum: ['android', 'ios', 'ANDROID', 'IOS'], description: '筛选平台' })
|
||||||
@ApiQuery({ name: 'includeDisabled', required: false, type: Boolean, description: '是否包含已禁用版本' })
|
@ApiQuery({ name: 'includeDisabled', required: false, type: Boolean, description: '是否包含已禁用版本' })
|
||||||
@ApiResponse({ status: 200, type: [VersionDto] })
|
@ApiResponse({ status: 200, type: [VersionDto] })
|
||||||
async listVersions(
|
async listVersions(
|
||||||
@Query('platform') platform?: Platform,
|
@Query('platform') platform?: string,
|
||||||
@Query('includeDisabled') includeDisabled?: string,
|
@Query('includeDisabled') includeDisabled?: string,
|
||||||
): Promise<VersionDto[]> {
|
): Promise<VersionDto[]> {
|
||||||
const query = new ListVersionsQuery(platform, includeDisabled === 'true')
|
// Convert platform to uppercase enum value
|
||||||
|
const platformEnum = platform ? (platform.toUpperCase() as Platform) : undefined
|
||||||
|
const query = new ListVersionsQuery(platformEnum, includeDisabled === 'true')
|
||||||
const versions = await this.listVersionsHandler.execute(query)
|
const versions = await this.listVersionsHandler.execute(query)
|
||||||
return versions.map((v) => this.toVersionDto(v))
|
return versions.map((v) => this.toVersionDto(v))
|
||||||
}
|
}
|
||||||
|
|
@ -202,7 +204,7 @@ export class VersionController {
|
||||||
@ApiBody({
|
@ApiBody({
|
||||||
schema: {
|
schema: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
required: ['file', 'platform', 'versionCode', 'versionName', 'buildNumber', 'changelog'],
|
required: ['file', 'platform', 'changelog'],
|
||||||
properties: {
|
properties: {
|
||||||
file: {
|
file: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
|
@ -216,16 +218,16 @@ export class VersionController {
|
||||||
},
|
},
|
||||||
versionCode: {
|
versionCode: {
|
||||||
type: 'integer',
|
type: 'integer',
|
||||||
description: '版本号',
|
description: '版本号 (可选,可从APK/IPA自动检测)',
|
||||||
minimum: 1,
|
minimum: 1,
|
||||||
},
|
},
|
||||||
versionName: {
|
versionName: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: '版本名称 (格式: x.y.z)',
|
description: '版本名称 (可选,可从APK/IPA自动检测,格式: x.y.z)',
|
||||||
},
|
},
|
||||||
buildNumber: {
|
buildNumber: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: '构建号',
|
description: '构建号 (可选,可从APK/IPA自动检测)',
|
||||||
},
|
},
|
||||||
changelog: {
|
changelog: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
|
@ -238,7 +240,7 @@ export class VersionController {
|
||||||
},
|
},
|
||||||
minOsVersion: {
|
minOsVersion: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: '最低操作系统版本',
|
description: '最低操作系统版本 (可选,可从APK/IPA自动检测)',
|
||||||
},
|
},
|
||||||
releaseDate: {
|
releaseDate: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
|
@ -271,16 +273,16 @@ export class VersionController {
|
||||||
|
|
||||||
const command = new UploadVersionCommand(
|
const command = new UploadVersionCommand(
|
||||||
dto.platform,
|
dto.platform,
|
||||||
|
file.buffer,
|
||||||
|
file.originalname,
|
||||||
|
dto.changelog,
|
||||||
|
dto.isForceUpdate ?? false,
|
||||||
|
'admin', // TODO: Get from JWT token
|
||||||
dto.versionCode,
|
dto.versionCode,
|
||||||
dto.versionName,
|
dto.versionName,
|
||||||
dto.buildNumber,
|
dto.buildNumber,
|
||||||
dto.changelog,
|
dto.minOsVersion,
|
||||||
dto.isForceUpdate ?? false,
|
|
||||||
dto.minOsVersion ?? null,
|
|
||||||
dto.releaseDate ? new Date(dto.releaseDate) : null,
|
dto.releaseDate ? new Date(dto.releaseDate) : null,
|
||||||
file.buffer,
|
|
||||||
file.originalname,
|
|
||||||
'admin', // TODO: Get from JWT token
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const version = await this.uploadVersionHandler.execute(command)
|
const version = await this.uploadVersionHandler.execute(command)
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
import { ApiProperty } from '@nestjs/swagger'
|
import { ApiProperty } from '@nestjs/swagger'
|
||||||
import { IsEnum, IsInt, Min } from 'class-validator'
|
import { IsEnum, IsInt, Min } from 'class-validator'
|
||||||
|
import { Transform } from 'class-transformer'
|
||||||
import { Platform } from '@/domain/enums/platform.enum'
|
import { Platform } from '@/domain/enums/platform.enum'
|
||||||
|
|
||||||
export class CheckUpdateDto {
|
export class CheckUpdateDto {
|
||||||
@ApiProperty({ enum: Platform, description: '平台类型' })
|
@ApiProperty({ enum: ['android', 'ios', 'ANDROID', 'IOS'], description: '平台类型' })
|
||||||
|
@Transform(({ value }) => (typeof value === 'string' ? value.toUpperCase() : value))
|
||||||
@IsEnum(Platform)
|
@IsEnum(Platform)
|
||||||
platform: Platform
|
platform: Platform
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'
|
||||||
import { IsEnum, IsInt, IsString, IsBoolean, IsUrl, Min, IsOptional, IsDateString } from 'class-validator'
|
import { IsEnum, IsInt, IsString, IsBoolean, IsUrl, Min, IsOptional, IsDateString } from 'class-validator'
|
||||||
|
import { Transform } from 'class-transformer'
|
||||||
import { Platform } from '@/domain/enums/platform.enum'
|
import { Platform } from '@/domain/enums/platform.enum'
|
||||||
|
|
||||||
export class CreateVersionDto {
|
export class CreateVersionDto {
|
||||||
@ApiProperty({ enum: Platform, description: '平台类型' })
|
@ApiProperty({ enum: ['android', 'ios', 'ANDROID', 'IOS'], description: '平台类型' })
|
||||||
|
@Transform(({ value }) => (typeof value === 'string' ? value.toUpperCase() : value))
|
||||||
@IsEnum(Platform)
|
@IsEnum(Platform)
|
||||||
platform: Platform
|
platform: Platform
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,31 @@
|
||||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'
|
||||||
import { IsString, IsInt, IsBoolean, IsOptional, IsDateString, Min, Matches } from 'class-validator'
|
import { IsString, IsInt, IsBoolean, IsOptional, IsDateString, Min, Matches, IsEnum } from 'class-validator'
|
||||||
import { Type, Transform } from 'class-transformer'
|
import { Type, Transform } from 'class-transformer'
|
||||||
import { Platform } from '@/domain/enums/platform.enum'
|
import { Platform } from '@/domain/enums/platform.enum'
|
||||||
|
|
||||||
export class UploadVersionDto {
|
export class UploadVersionDto {
|
||||||
@ApiProperty({ enum: Platform, description: '平台 (android/ios)' })
|
@ApiProperty({ enum: ['android', 'ios', 'ANDROID', 'IOS'], description: '平台 (android/ios)' })
|
||||||
@IsString()
|
@Transform(({ value }) => (typeof value === 'string' ? value.toUpperCase() : value))
|
||||||
|
@IsEnum(Platform)
|
||||||
platform: Platform
|
platform: Platform
|
||||||
|
|
||||||
@ApiProperty({ description: '版本号', example: 100, minimum: 1 })
|
@ApiPropertyOptional({ description: '版本号 (可从APK/IPA自动检测)', example: 100, minimum: 1 })
|
||||||
|
@IsOptional()
|
||||||
@Type(() => Number)
|
@Type(() => Number)
|
||||||
@IsInt()
|
@IsInt()
|
||||||
@Min(1)
|
@Min(1)
|
||||||
versionCode: number
|
versionCode?: number
|
||||||
|
|
||||||
@ApiProperty({ description: '版本名称', example: '1.0.0' })
|
@ApiPropertyOptional({ description: '版本名称 (可从APK/IPA自动检测)', example: '1.0.0' })
|
||||||
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
@Matches(/^\d+\.\d+\.\d+$/, { message: 'versionName must be in format x.y.z' })
|
@Matches(/^\d+\.\d+\.\d+$/, { message: 'versionName must be in format x.y.z' })
|
||||||
versionName: string
|
versionName?: string
|
||||||
|
|
||||||
@ApiProperty({ description: '构建号', example: '100' })
|
@ApiPropertyOptional({ description: '构建号 (可从APK/IPA自动检测)', example: '100' })
|
||||||
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
buildNumber: string
|
buildNumber?: string
|
||||||
|
|
||||||
@ApiProperty({ description: '更新日志' })
|
@ApiProperty({ description: '更新日志' })
|
||||||
@IsString()
|
@IsString()
|
||||||
|
|
@ -33,7 +37,7 @@ export class UploadVersionDto {
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
isForceUpdate?: boolean
|
isForceUpdate?: boolean
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: '最低操作系统版本', example: '10.0' })
|
@ApiPropertyOptional({ description: '最低操作系统版本 (可从APK/IPA自动检测)', example: '10.0' })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
minOsVersion?: string
|
minOsVersion?: string
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { AppVersionMapper } from './infrastructure/persistence/mappers/app-versi
|
||||||
import { AppVersionRepositoryImpl } from './infrastructure/persistence/repositories/app-version.repository.impl';
|
import { AppVersionRepositoryImpl } from './infrastructure/persistence/repositories/app-version.repository.impl';
|
||||||
import { APP_VERSION_REPOSITORY } from './domain/repositories/app-version.repository';
|
import { APP_VERSION_REPOSITORY } from './domain/repositories/app-version.repository';
|
||||||
import { FileStorageService } from './infrastructure/storage/file-storage.service';
|
import { FileStorageService } from './infrastructure/storage/file-storage.service';
|
||||||
|
import { PackageParserService } from './infrastructure/parsers/package-parser.service';
|
||||||
import { CheckUpdateHandler } from './application/queries/check-update/check-update.handler';
|
import { CheckUpdateHandler } from './application/queries/check-update/check-update.handler';
|
||||||
import { ListVersionsHandler } from './application/queries/list-versions/list-versions.handler';
|
import { ListVersionsHandler } from './application/queries/list-versions/list-versions.handler';
|
||||||
import { GetVersionHandler } from './application/queries/get-version/get-version.handler';
|
import { GetVersionHandler } from './application/queries/get-version/get-version.handler';
|
||||||
|
|
@ -37,6 +38,7 @@ import { HealthController } from './api/controllers/health.controller';
|
||||||
PrismaService,
|
PrismaService,
|
||||||
AppVersionMapper,
|
AppVersionMapper,
|
||||||
FileStorageService,
|
FileStorageService,
|
||||||
|
PackageParserService,
|
||||||
{
|
{
|
||||||
provide: APP_VERSION_REPOSITORY,
|
provide: APP_VERSION_REPOSITORY,
|
||||||
useClass: AppVersionRepositoryImpl,
|
useClass: AppVersionRepositoryImpl,
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,16 @@ import { Platform } from '@/domain/enums/platform.enum'
|
||||||
export class UploadVersionCommand {
|
export class UploadVersionCommand {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly platform: Platform,
|
public readonly platform: Platform,
|
||||||
public readonly versionCode: number,
|
|
||||||
public readonly versionName: string,
|
|
||||||
public readonly buildNumber: string,
|
|
||||||
public readonly changelog: string,
|
|
||||||
public readonly isForceUpdate: boolean,
|
|
||||||
public readonly minOsVersion: string | null,
|
|
||||||
public readonly releaseDate: Date | null,
|
|
||||||
public readonly fileBuffer: Buffer,
|
public readonly fileBuffer: Buffer,
|
||||||
public readonly originalFilename: string,
|
public readonly originalFilename: string,
|
||||||
|
public readonly changelog: string,
|
||||||
|
public readonly isForceUpdate: boolean,
|
||||||
public readonly createdBy: string,
|
public readonly createdBy: string,
|
||||||
|
// Optional fields - can be auto-detected from package
|
||||||
|
public readonly versionCode?: number,
|
||||||
|
public readonly versionName?: string,
|
||||||
|
public readonly buildNumber?: string,
|
||||||
|
public readonly minOsVersion?: string | null,
|
||||||
|
public readonly releaseDate?: Date | null,
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common'
|
import { Inject, Injectable, BadRequestException } from '@nestjs/common'
|
||||||
import { UploadVersionCommand } from './upload-version.command'
|
import { UploadVersionCommand } from './upload-version.command'
|
||||||
import { AppVersionRepository, APP_VERSION_REPOSITORY } from '@/domain/repositories/app-version.repository'
|
import { AppVersionRepository, APP_VERSION_REPOSITORY } from '@/domain/repositories/app-version.repository'
|
||||||
import { AppVersion } from '@/domain/entities/app-version.entity'
|
import { AppVersion } from '@/domain/entities/app-version.entity'
|
||||||
|
|
@ -11,6 +11,7 @@ import { FileSha256 } from '@/domain/value-objects/file-sha256.vo'
|
||||||
import { Changelog } from '@/domain/value-objects/changelog.vo'
|
import { Changelog } from '@/domain/value-objects/changelog.vo'
|
||||||
import { MinOsVersion } from '@/domain/value-objects/min-os-version.vo'
|
import { MinOsVersion } from '@/domain/value-objects/min-os-version.vo'
|
||||||
import { FileStorageService } from '@/infrastructure/storage/file-storage.service'
|
import { FileStorageService } from '@/infrastructure/storage/file-storage.service'
|
||||||
|
import { PackageParserService } from '@/infrastructure/parsers/package-parser.service'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UploadVersionHandler {
|
export class UploadVersionHandler {
|
||||||
|
|
@ -18,40 +19,65 @@ export class UploadVersionHandler {
|
||||||
@Inject(APP_VERSION_REPOSITORY)
|
@Inject(APP_VERSION_REPOSITORY)
|
||||||
private readonly appVersionRepository: AppVersionRepository,
|
private readonly appVersionRepository: AppVersionRepository,
|
||||||
private readonly fileStorageService: FileStorageService,
|
private readonly fileStorageService: FileStorageService,
|
||||||
|
private readonly packageParserService: PackageParserService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async execute(command: UploadVersionCommand): Promise<AppVersion> {
|
async execute(command: UploadVersionCommand): Promise<AppVersion> {
|
||||||
|
// Parse package info from APK/IPA
|
||||||
|
const parsedInfo = await this.packageParserService.parsePackage(
|
||||||
|
command.fileBuffer,
|
||||||
|
command.platform,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Determine version info (use provided values or fall back to parsed values)
|
||||||
|
const versionCode = command.versionCode ?? parsedInfo?.versionCode
|
||||||
|
const versionName = command.versionName ?? parsedInfo?.versionName
|
||||||
|
const buildNumber = command.buildNumber ?? versionCode?.toString()
|
||||||
|
const minOsVersion = command.minOsVersion ?? parsedInfo?.minSdkVersion
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if (!versionCode) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
'Unable to detect version code from package. Please provide it manually.',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (!versionName) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
'Unable to detect version name from package. Please provide it manually.',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Save the uploaded file
|
// Save the uploaded file
|
||||||
const uploadResult = await this.fileStorageService.saveFile(
|
const uploadResult = await this.fileStorageService.saveFile(
|
||||||
command.fileBuffer,
|
command.fileBuffer,
|
||||||
command.originalFilename,
|
command.originalFilename,
|
||||||
command.platform,
|
command.platform,
|
||||||
command.versionName,
|
versionName,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Create value objects
|
// Create value objects
|
||||||
const versionCode = VersionCode.create(command.versionCode)
|
const versionCodeVO = VersionCode.create(versionCode)
|
||||||
const versionName = VersionName.create(command.versionName)
|
const versionNameVO = VersionName.create(versionName)
|
||||||
const buildNumber = BuildNumber.create(command.buildNumber)
|
const buildNumberVO = BuildNumber.create(buildNumber || versionCode.toString())
|
||||||
const downloadUrl = DownloadUrl.create(uploadResult.url)
|
const downloadUrl = DownloadUrl.create(uploadResult.url)
|
||||||
const fileSize = FileSize.create(BigInt(uploadResult.size))
|
const fileSize = FileSize.create(BigInt(uploadResult.size))
|
||||||
const fileSha256 = FileSha256.create(uploadResult.sha256)
|
const fileSha256 = FileSha256.create(uploadResult.sha256)
|
||||||
const changelog = Changelog.create(command.changelog)
|
const changelog = Changelog.create(command.changelog)
|
||||||
const minOsVersion = command.minOsVersion ? MinOsVersion.create(command.minOsVersion) : null
|
const minOsVersionVO = minOsVersion ? MinOsVersion.create(minOsVersion) : null
|
||||||
|
|
||||||
// Create the app version entity
|
// Create the app version entity
|
||||||
const appVersion = AppVersion.create({
|
const appVersion = AppVersion.create({
|
||||||
platform: command.platform,
|
platform: command.platform,
|
||||||
versionCode,
|
versionCode: versionCodeVO,
|
||||||
versionName,
|
versionName: versionNameVO,
|
||||||
buildNumber,
|
buildNumber: buildNumberVO,
|
||||||
downloadUrl,
|
downloadUrl,
|
||||||
fileSize,
|
fileSize,
|
||||||
fileSha256,
|
fileSha256,
|
||||||
changelog,
|
changelog,
|
||||||
isForceUpdate: command.isForceUpdate,
|
isForceUpdate: command.isForceUpdate,
|
||||||
minOsVersion,
|
minOsVersion: minOsVersionVO,
|
||||||
releaseDate: command.releaseDate,
|
releaseDate: command.releaseDate ?? null,
|
||||||
createdBy: command.createdBy,
|
createdBy: command.createdBy,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
import { Injectable, Logger } from '@nestjs/common'
|
||||||
|
import * as ApkReader from 'adbkit-apkreader'
|
||||||
|
import * as unzipper from 'unzipper'
|
||||||
|
import * as bplist from 'bplist-parser'
|
||||||
|
import { Platform } from '@/domain/enums/platform.enum'
|
||||||
|
|
||||||
|
export interface ParsedPackageInfo {
|
||||||
|
packageName: string
|
||||||
|
versionCode: number
|
||||||
|
versionName: string
|
||||||
|
minSdkVersion?: string
|
||||||
|
targetSdkVersion?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PackageParserService {
|
||||||
|
private readonly logger = new Logger(PackageParserService.name)
|
||||||
|
|
||||||
|
async parsePackage(buffer: Buffer, platform: Platform): Promise<ParsedPackageInfo | null> {
|
||||||
|
try {
|
||||||
|
if (platform === Platform.ANDROID) {
|
||||||
|
return await this.parseApk(buffer)
|
||||||
|
} else if (platform === Platform.IOS) {
|
||||||
|
return await this.parseIpa(buffer)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Failed to parse package: ${error.message}`, error.stack)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async parseApk(buffer: Buffer): Promise<ParsedPackageInfo | null> {
|
||||||
|
try {
|
||||||
|
const reader = await ApkReader.open(buffer)
|
||||||
|
const manifest = await reader.readManifest()
|
||||||
|
|
||||||
|
return {
|
||||||
|
packageName: manifest.package,
|
||||||
|
versionCode: manifest.versionCode,
|
||||||
|
versionName: manifest.versionName,
|
||||||
|
minSdkVersion: manifest.usesSdk?.minSdkVersion?.toString(),
|
||||||
|
targetSdkVersion: manifest.usesSdk?.targetSdkVersion?.toString(),
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Failed to parse APK: ${error.message}`)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async parseIpa(buffer: Buffer): Promise<ParsedPackageInfo | null> {
|
||||||
|
try {
|
||||||
|
const directory = await unzipper.Open.buffer(buffer)
|
||||||
|
|
||||||
|
// Find Info.plist in the .app directory
|
||||||
|
const plistFile = directory.files.find(
|
||||||
|
(file: { path: string }) => file.path.match(/Payload\/[^/]+\.app\/Info\.plist$/)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!plistFile) {
|
||||||
|
throw new Error('Info.plist not found in IPA')
|
||||||
|
}
|
||||||
|
|
||||||
|
const plistBuffer = await plistFile.buffer()
|
||||||
|
|
||||||
|
// Try to parse as binary plist first
|
||||||
|
let plistData: any
|
||||||
|
try {
|
||||||
|
const parsed = bplist.parseBuffer(plistBuffer)
|
||||||
|
plistData = parsed[0]
|
||||||
|
} catch {
|
||||||
|
// If binary parsing fails, try XML parsing
|
||||||
|
const plistContent = plistBuffer.toString('utf8')
|
||||||
|
plistData = this.parseXmlPlist(plistContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!plistData) {
|
||||||
|
throw new Error('Failed to parse Info.plist')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
packageName: plistData.CFBundleIdentifier || '',
|
||||||
|
versionCode: parseInt(plistData.CFBundleVersion || '0', 10),
|
||||||
|
versionName: plistData.CFBundleShortVersionString || plistData.CFBundleVersion || '',
|
||||||
|
minSdkVersion: plistData.MinimumOSVersion,
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Failed to parse IPA: ${error.message}`)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseXmlPlist(content: string): Record<string, any> | null {
|
||||||
|
try {
|
||||||
|
const result: Record<string, any> = {}
|
||||||
|
const keyRegex = /<key>([^<]+)<\/key>\s*<string>([^<]+)<\/string>/g
|
||||||
|
let match
|
||||||
|
|
||||||
|
while ((match = keyRegex.exec(content)) !== null) {
|
||||||
|
result[match[1]] = match[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also try to match integer values
|
||||||
|
const intRegex = /<key>([^<]+)<\/key>\s*<integer>([^<]+)<\/integer>/g
|
||||||
|
while ((match = intRegex.exec(content)) !== null) {
|
||||||
|
result[match[1]] = match[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(result).length > 0 ? result : null
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
declare module 'adbkit-apkreader' {
|
||||||
|
interface UsesSdk {
|
||||||
|
minSdkVersion?: number
|
||||||
|
targetSdkVersion?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Manifest {
|
||||||
|
package: string
|
||||||
|
versionCode: number
|
||||||
|
versionName: string
|
||||||
|
usesSdk?: UsesSdk
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApkReader {
|
||||||
|
readManifest(): Promise<Manifest>
|
||||||
|
}
|
||||||
|
|
||||||
|
function open(source: string | Buffer): Promise<ApkReader>
|
||||||
|
|
||||||
|
export = {
|
||||||
|
open,
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue