├── .editorconfig ├── .github └── workflows │ ├── build-test.yaml │ └── publish-docker.yaml ├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── Dockerfile ├── LICENSE ├── MaaDownloadServer.Build ├── BuildContext.cs ├── MaaDownloadServer.Build.csproj ├── Program.cs └── Tasks │ ├── BuildTask.cs │ ├── CleanTask.cs │ ├── DefaultTask.cs │ ├── LoggingTask.cs │ ├── PostPublishTask.cs │ └── PublishTask.cs ├── MaaDownloadServer.LocalTest ├── .gitignore └── MaaDownloadServer.LocalTest.csproj ├── MaaDownloadServer.sln ├── MaaDownloadServer ├── Controller │ ├── AnnounceController.cs │ ├── ComponentController.cs │ ├── DownloadController.cs │ ├── ListController.cs │ └── VersionController.cs ├── Database │ ├── DbContextExtension.cs │ └── MaaDownloadServerDbContext.cs ├── Enums │ ├── AnnounceLevel.cs │ ├── Architecture.cs │ ├── ChecksumType.cs │ ├── Platform.cs │ ├── ProgramExitCode.cs │ └── PublicContentTagType.cs ├── Extensions │ ├── FileSystemExtension.cs │ ├── HttpClientFactoryExtension.cs │ ├── OptionExtension.cs │ ├── PlatformArchExtension.cs │ ├── SemanticVersionExtension.cs │ └── ServiceExtension.cs ├── External │ └── Python.cs ├── Global.cs ├── Jobs │ ├── JobExtension.cs │ ├── PackageUpdateJob.cs │ └── PublicContentCheckJob.cs ├── MaaDownloadServer.csproj ├── MaaDownloadServer.csproj.DotSettings ├── Middleware │ └── DownloadCountMiddleware.cs ├── Migrations │ ├── 20220304022625_Initialize.Designer.cs │ ├── 20220304022625_Initialize.cs │ ├── 20220307161059_FixNameTypo.Designer.cs │ ├── 20220307161059_FixNameTypo.cs │ ├── 20220316053837_RemoveGameData.Designer.cs │ ├── 20220316053837_RemoveGameData.cs │ ├── 20220318190006_AddDownloadCount.Designer.cs │ ├── 20220318190006_AddDownloadCount.cs │ └── MaaDownloadServerDbContextModelSnapshot.cs ├── Model │ ├── Attributes │ │ ├── ConfigurationSectionAttribute.cs │ │ └── MaaAttribute.cs │ ├── Dto │ │ ├── Announce │ │ │ └── Announce.cs │ │ ├── ComponentController │ │ │ ├── ComponentDto.cs │ │ │ └── GetComponentDetailDto.cs │ │ ├── DownloadController │ │ │ └── GetDownloadUrlDto.cs │ │ ├── General │ │ │ ├── ComponentSupport.cs │ │ │ ├── ComponentVersions.cs │ │ │ ├── ResourceMetadata.cs │ │ │ └── VersionDetail.cs │ │ └── VersionController │ │ │ └── GetVersionDto.cs │ ├── Entities │ │ ├── DatabaseCache.cs │ │ ├── DownloadCount.cs │ │ ├── Package.cs │ │ ├── PublicContent.cs │ │ └── Resource.cs │ ├── External │ │ └── Script │ │ │ ├── AfterDownloadProcessOperation.cs │ │ │ ├── BeforeAddProcessOperation.cs │ │ │ ├── ComponentConfiguration.cs │ │ │ ├── PreProcess.cs │ │ │ └── Scripts.cs │ ├── General │ │ ├── DownloadContentInfo.cs │ │ ├── PublicContentTag.cs │ │ ├── ResourceInfo.cs │ │ └── UpdateDiff.cs │ └── Options │ │ ├── AnnounceOption.cs │ │ ├── DataDirectoriesOption.cs │ │ ├── IMaaOption.cs │ │ ├── NetworkOption.cs │ │ ├── PublicContentOption.cs │ │ ├── ScriptEngineOption.cs │ │ ├── ServerOption.cs │ │ └── SubOptions │ │ └── DataDirectoriesSubDirectoriesOption.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── Providers │ └── MaaConfigurationProvider.cs ├── Services │ ├── Base │ │ ├── AnnounceService.cs │ │ ├── ConfigurationService.cs │ │ ├── FileSystemService.cs │ │ └── Interfaces │ │ │ ├── IAnnounceService.cs │ │ │ ├── IConfigurationService.cs │ │ │ └── IFileSystemService.cs │ └── Controller │ │ ├── ComponentService.cs │ │ ├── DownloadService.cs │ │ ├── Interfaces │ │ ├── IComponentService.cs │ │ ├── IDownloadService.cs │ │ └── IVersionService.cs │ │ └── VersionService.cs ├── Utils │ ├── AttributeUtil.cs │ ├── HashUtil.cs │ └── PublicContentTagUtil.cs ├── appsettings.Docker.json └── appsettings.json ├── README.md ├── demo ├── README.md ├── component.json └── get_download_info.py ├── docs ├── Compile.md ├── ComponentAndPythonScript.md ├── RunDocker.md └── RunNative.md ├── publish-docker.ps1 ├── publish-docker.sh ├── publish.ps1 ├── publish.sh └── push-docker.sh /.github/workflows/build-test.yaml: -------------------------------------------------------------------------------- 1 | name: build-test 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | 7 | defaults: 8 | run: 9 | shell: "bash" 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-20.04 14 | steps: 15 | - name: "Check out" 16 | uses: actions/checkout@v2 17 | 18 | - name: "Setup .NET SDK" 19 | uses: actions/setup-dotnet@v1 20 | with: 21 | dotnet-version: "6.0.x" 22 | 23 | - name: "Run build & publish script" 24 | run: ./publish.sh 25 | -------------------------------------------------------------------------------- /.github/workflows/publish-docker.yaml: -------------------------------------------------------------------------------- 1 | name: publish-docker 2 | on: 3 | release: 4 | types: [published] 5 | 6 | defaults: 7 | run: 8 | shell: "bash" 9 | 10 | jobs: 11 | publish-docker: 12 | runs-on: ubuntu-20.04 13 | steps: 14 | - name: "Check out" 15 | uses: actions/checkout@v2 16 | 17 | - name: "Set up QEMU" 18 | uses: docker/setup-qemu-action@v1 19 | 20 | - name: "Set up Docker Buildx" 21 | uses: docker/setup-buildx-action@v1 22 | 23 | - name: "Setup .NET SDK" 24 | uses: actions/setup-dotnet@v1 25 | with: 26 | dotnet-version: "6.0.x" 27 | 28 | - name: "Echo current version number" 29 | run: echo "Current version is ${{ github.event.release.tag_name }}" 30 | 31 | - name: "Run publish docker script" 32 | run: ./publish-docker.sh ${{ github.event.release.tag_name }} 33 | 34 | - name: "Login to Docker Hub" 35 | uses: docker/login-action@v1 36 | with: 37 | username: ${{ secrets.DOCKERHUB_USERNAME }} 38 | password: ${{ secrets.DOCKERHUB_TOKEN }} 39 | 40 | - name: "Push image to Docker Hub" 41 | run: ./push-docker.sh ${{ github.event.release.tag_name }} alisaqaq 42 | 43 | - name: "Login to Tencent Cloud TCR" 44 | uses: docker/login-action@v1 45 | with: 46 | registry: ccr.ccs.tencentyun.com 47 | username: ${{ secrets.TENCENT_CLOUD_USERNAME }} 48 | password: ${{ secrets.TENCENT_CLOUD_TOKEN }} 49 | 50 | - name: "Push image to Tencent Cloud TCR" 51 | run: ./push-docker.sh ${{ github.event.release.tag_name }} ccr.ccs.tencentyun.com/alisaqaq 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### JetBrains template 2 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 3 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 4 | 5 | # User-specific stuff 6 | .idea/**/workspace.xml 7 | .idea/**/tasks.xml 8 | .idea/**/usage.statistics.xml 9 | .idea/**/dictionaries 10 | .idea/**/shelf 11 | 12 | # Generated files 13 | .idea/**/contentModel.xml 14 | 15 | # Sensitive or high-churn files 16 | .idea/**/dataSources/ 17 | .idea/**/dataSources.ids 18 | .idea/**/dataSources.local.xml 19 | .idea/**/sqlDataSources.xml 20 | .idea/**/dynamic.xml 21 | .idea/**/uiDesigner.xml 22 | .idea/**/dbnavigator.xml 23 | 24 | # Gradle 25 | .idea/**/gradle.xml 26 | .idea/**/libraries 27 | 28 | .idea 29 | 30 | # Gradle and Maven with auto-import 31 | # When using Gradle or Maven with auto-import, you should exclude module files, 32 | # since they will be recreated, and may cause churn. Uncomment if using 33 | # auto-import. 34 | # .idea/artifacts 35 | # .idea/compiler.xml 36 | # .idea/jarRepositories.xml 37 | # .idea/modules.xml 38 | # .idea/*.iml 39 | # .idea/modules 40 | # *.iml 41 | # *.ipr 42 | 43 | # CMake 44 | cmake-build-*/ 45 | 46 | # Mongo Explorer plugin 47 | .idea/**/mongoSettings.xml 48 | 49 | # File-based project format 50 | *.iws 51 | 52 | # IntelliJ 53 | out/ 54 | 55 | # mpeltonen/sbt-idea plugin 56 | .idea_modules/ 57 | 58 | # JIRA plugin 59 | atlassian-ide-plugin.xml 60 | 61 | # Cursive Clojure plugin 62 | .idea/replstate.xml 63 | 64 | # Crashlytics plugin (for Android Studio and IntelliJ) 65 | com_crashlytics_export_strings.xml 66 | crashlytics.properties 67 | crashlytics-build.properties 68 | fabric.properties 69 | 70 | # Editor-based Rest Client 71 | .idea/httpRequests 72 | 73 | # Android studio 3.1+ serialized cache file 74 | .idea/caches/build_file_checksums.ser 75 | 76 | ### macOS template 77 | # General 78 | .DS_Store 79 | .AppleDouble 80 | .LSOverride 81 | 82 | # Icon must end with two \r 83 | Icon 84 | 85 | # Thumbnails 86 | ._* 87 | 88 | # Files that might appear in the root of a volume 89 | .DocumentRevisions-V100 90 | .fseventsd 91 | .Spotlight-V100 92 | .TemporaryItems 93 | .Trashes 94 | .VolumeIcon.icns 95 | .com.apple.timemachine.donotpresent 96 | 97 | # Directories potentially created on remote AFP share 98 | .AppleDB 99 | .AppleDesktop 100 | Network Trash Folder 101 | Temporary Items 102 | .apdisk 103 | 104 | ### VisualStudio template 105 | ## Ignore Visual Studio temporary files, build results, and 106 | ## files generated by popular Visual Studio add-ons. 107 | ## 108 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 109 | 110 | # User-specific files 111 | *.rsuser 112 | *.suo 113 | *.user 114 | *.userosscache 115 | *.sln.docstates 116 | 117 | # User-specific files (MonoDevelop/Xamarin Studio) 118 | *.userprefs 119 | 120 | # Mono auto generated files 121 | mono_crash.* 122 | 123 | # Build results 124 | [Dd]ebug/ 125 | [Dd]ebugPublic/ 126 | [Rr]elease/ 127 | [Rr]eleases/ 128 | x64/ 129 | x86/ 130 | [Ww][Ii][Nn]32/ 131 | [Aa][Rr][Mm]/ 132 | [Aa][Rr][Mm]64/ 133 | bld/ 134 | [Bb]in/ 135 | [Oo]bj/ 136 | [Ll]og/ 137 | [Ll]ogs/ 138 | 139 | # Visual Studio 2015/2017 cache/options directory 140 | .vs/ 141 | # Uncomment if you have tasks that create the project's static files in wwwroot 142 | #wwwroot/ 143 | 144 | # Visual Studio 2017 auto generated files 145 | Generated\ Files/ 146 | 147 | # MSTest test Results 148 | [Tt]est[Rr]esult*/ 149 | [Bb]uild[Ll]og.* 150 | 151 | # NUnit 152 | *.VisualState.xml 153 | TestResult.xml 154 | nunit-*.xml 155 | 156 | # Build Results of an ATL Project 157 | [Dd]ebugPS/ 158 | [Rr]eleasePS/ 159 | dlldata.c 160 | 161 | # Benchmark Results 162 | BenchmarkDotNet.Artifacts/ 163 | 164 | # .NET Core 165 | project.lock.json 166 | project.fragment.lock.json 167 | artifacts/ 168 | 169 | # ASP.NET Scaffolding 170 | ScaffoldingReadMe.txt 171 | 172 | # StyleCop 173 | StyleCopReport.xml 174 | 175 | # Files built by Visual Studio 176 | *_i.c 177 | *_p.c 178 | *_h.h 179 | *.ilk 180 | *.meta 181 | *.obj 182 | *.iobj 183 | *.pch 184 | *.pdb 185 | *.ipdb 186 | *.pgc 187 | *.pgd 188 | *.rsp 189 | *.sbr 190 | *.tlb 191 | *.tli 192 | *.tlh 193 | *.tmp 194 | *.tmp_proj 195 | *_wpftmp.csproj 196 | *.log 197 | *.vspscc 198 | *.vssscc 199 | .builds 200 | *.pidb 201 | *.svclog 202 | *.scc 203 | 204 | # Chutzpah Test files 205 | _Chutzpah* 206 | 207 | # Visual C++ cache files 208 | ipch/ 209 | *.aps 210 | *.ncb 211 | *.opendb 212 | *.opensdf 213 | *.sdf 214 | *.cachefile 215 | *.VC.db 216 | *.VC.VC.opendb 217 | 218 | # Visual Studio profiler 219 | *.psess 220 | *.vsp 221 | *.vspx 222 | *.sap 223 | 224 | # Visual Studio Trace Files 225 | *.e2e 226 | 227 | # TFS 2012 Local Workspace 228 | $tf/ 229 | 230 | # Guidance Automation Toolkit 231 | *.gpState 232 | 233 | # ReSharper is a .NET coding add-in 234 | _ReSharper*/ 235 | *.[Rr]e[Ss]harper 236 | *.DotSettings.user 237 | 238 | # TeamCity is a build add-in 239 | _TeamCity* 240 | 241 | # DotCover is a Code Coverage Tool 242 | *.dotCover 243 | 244 | # AxoCover is a Code Coverage Tool 245 | .axoCover/* 246 | !.axoCover/settings.json 247 | 248 | # Coverlet is a free, cross platform Code Coverage Tool 249 | coverage*.json 250 | coverage*.xml 251 | coverage*.info 252 | 253 | # Visual Studio code coverage results 254 | *.coverage 255 | *.coveragexml 256 | 257 | # NCrunch 258 | _NCrunch_* 259 | .*crunch*.local.xml 260 | nCrunchTemp_* 261 | 262 | # MightyMoose 263 | *.mm.* 264 | AutoTest.Net/ 265 | 266 | # Web workbench (sass) 267 | .sass-cache/ 268 | 269 | # Installshield output folder 270 | [Ee]xpress/ 271 | 272 | # DocProject is a documentation generator add-in 273 | DocProject/buildhelp/ 274 | DocProject/Help/*.HxT 275 | DocProject/Help/*.HxC 276 | DocProject/Help/*.hhc 277 | DocProject/Help/*.hhk 278 | DocProject/Help/*.hhp 279 | DocProject/Help/Html2 280 | DocProject/Help/html 281 | 282 | # Click-Once directory 283 | publish/ 284 | 285 | # Publish Web Output 286 | *.[Pp]ublish.xml 287 | *.azurePubxml 288 | # Note: Comment the next line if you want to checkin your web deploy settings, 289 | # but database connection strings (with potential passwords) will be unencrypted 290 | *.pubxml 291 | *.publishproj 292 | 293 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 294 | # checkin your Azure Web App publish settings, but sensitive information contained 295 | # in these scripts will be unencrypted 296 | PublishScripts/ 297 | 298 | # NuGet Packages 299 | *.nupkg 300 | # NuGet Symbol Packages 301 | *.snupkg 302 | # The packages folder can be ignored because of Package Restore 303 | **/[Pp]ackages/* 304 | # except build/, which is used as an MSBuild target. 305 | !**/[Pp]ackages/build/ 306 | # Uncomment if necessary however generally it will be regenerated when needed 307 | #!**/[Pp]ackages/repositories.config 308 | # NuGet v3's project.json files produces more ignorable files 309 | *.nuget.props 310 | *.nuget.targets 311 | 312 | # Microsoft Azure Build Output 313 | csx/ 314 | *.build.csdef 315 | 316 | # Microsoft Azure Emulator 317 | ecf/ 318 | rcf/ 319 | 320 | # Windows Store app package directories and files 321 | AppPackages/ 322 | BundleArtifacts/ 323 | Package.StoreAssociation.xml 324 | _pkginfo.txt 325 | *.appx 326 | *.appxbundle 327 | *.appxupload 328 | 329 | # Visual Studio cache files 330 | # files ending in .cache can be ignored 331 | *.[Cc]ache 332 | # but keep track of directories ending in .cache 333 | !?*.[Cc]ache/ 334 | 335 | # Others 336 | ClientBin/ 337 | ~$* 338 | *~ 339 | *.dbmdl 340 | *.dbproj.schemaview 341 | *.jfm 342 | *.pfx 343 | *.publishsettings 344 | orleans.codegen.cs 345 | 346 | # Including strong name files can present a security risk 347 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 348 | #*.snk 349 | 350 | # Since there are multiple workflows, uncomment next line to ignore bower_components 351 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 352 | #bower_components/ 353 | 354 | # RIA/Silverlight projects 355 | Generated_Code/ 356 | 357 | # Backup & report files from converting an old project file 358 | # to a newer Visual Studio version. Backup files are not needed, 359 | # because we have git ;-) 360 | _UpgradeReport_Files/ 361 | Backup*/ 362 | UpgradeLog*.XML 363 | UpgradeLog*.htm 364 | ServiceFabricBackup/ 365 | *.rptproj.bak 366 | 367 | # SQL Server files 368 | *.mdf 369 | *.ldf 370 | *.ndf 371 | 372 | # Business Intelligence projects 373 | *.rdl.data 374 | *.bim.layout 375 | *.bim_*.settings 376 | *.rptproj.rsuser 377 | *- [Bb]ackup.rdl 378 | *- [Bb]ackup ([0-9]).rdl 379 | *- [Bb]ackup ([0-9][0-9]).rdl 380 | 381 | # Microsoft Fakes 382 | FakesAssemblies/ 383 | 384 | # GhostDoc plugin setting file 385 | *.GhostDoc.xml 386 | 387 | # Node.js Tools for Visual Studio 388 | .ntvs_analysis.dat 389 | node_modules/ 390 | 391 | # Visual Studio 6 build log 392 | *.plg 393 | 394 | # Visual Studio 6 workspace options file 395 | *.opt 396 | 397 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 398 | *.vbw 399 | 400 | # Visual Studio LightSwitch build output 401 | **/*.HTMLClient/GeneratedArtifacts 402 | **/*.DesktopClient/GeneratedArtifacts 403 | **/*.DesktopClient/ModelManifest.xml 404 | **/*.Server/GeneratedArtifacts 405 | **/*.Server/ModelManifest.xml 406 | _Pvt_Extensions 407 | 408 | # Paket dependency manager 409 | .paket/paket.exe 410 | paket-files/ 411 | 412 | # FAKE - F# Make 413 | .fake/ 414 | 415 | # CodeRush personal settings 416 | .cr/personal 417 | 418 | # Python Tools for Visual Studio (PTVS) 419 | __pycache__/ 420 | *.pyc 421 | 422 | # Cake - Uncomment if you are using it 423 | # tools/** 424 | # !tools/packages.config 425 | 426 | # Tabs Studio 427 | *.tss 428 | 429 | # Telerik's JustMock configuration file 430 | *.jmconfig 431 | 432 | # BizTalk build output 433 | *.btp.cs 434 | *.btm.cs 435 | *.odx.cs 436 | *.xsd.cs 437 | 438 | # OpenCover UI analysis results 439 | OpenCover/ 440 | 441 | # Azure Stream Analytics local run output 442 | ASALocalRun/ 443 | 444 | # MSBuild Binary and Structured Log 445 | *.binlog 446 | 447 | # NVidia Nsight GPU debugger configuration file 448 | *.nvuser 449 | 450 | # MFractors (Xamarin productivity tool) working folder 451 | .mfractor/ 452 | 453 | # Local History for Visual Studio 454 | .localhistory/ 455 | 456 | # BeatPulse healthcheck temp database 457 | healthchecksdb 458 | 459 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 460 | MigrationBackup/ 461 | 462 | # Ionide (cross platform F# VS Code tools) working folder 463 | .ionide/ 464 | 465 | # Fody - auto-generated XML schema 466 | FodyWeavers.xsd 467 | 468 | ### MonoDevelop template 469 | #User Specific 470 | *.usertasks 471 | 472 | #Mono Project Files 473 | *.resources 474 | test-results/ 475 | 476 | ### Windows template 477 | # Windows thumbnail cache files 478 | Thumbs.db 479 | Thumbs.db:encryptable 480 | ehthumbs.db 481 | ehthumbs_vista.db 482 | 483 | # Dump file 484 | *.stackdump 485 | 486 | # Folder config file 487 | [Dd]esktop.ini 488 | 489 | # Recycle Bin used on file shares 490 | $RECYCLE.BIN/ 491 | 492 | # Windows Installer files 493 | *.cab 494 | *.msi 495 | *.msix 496 | *.msm 497 | *.msp 498 | 499 | # Windows shortcuts 500 | *.lnk 501 | 502 | ### VisualStudioCode template 503 | .vscode/* 504 | !.vscode/settings.json 505 | !.vscode/tasks.json 506 | !.vscode/launch.json 507 | !.vscode/extensions.json 508 | *.code-workspace 509 | 510 | # Local History for Visual Studio Code 511 | .history/ 512 | 513 | # Development configuration file 514 | *.Development.json 515 | 516 | # Database File 517 | *.db* 518 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | // Use IntelliSense to find out which attributes exist for C# debugging 6 | // Use hover for the description of the existing attributes 7 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 8 | "name": ".NET Core Launch (web)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/MaaDownloadServer/bin/Debug/net6.0/MaaDownloadServer.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}/publish", 16 | "stopAtEntry": false, 17 | // Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser 18 | // "serverReadyAction": { 19 | // "action": "openExternally", 20 | // "pattern": "\\bNow listening on:\\s+(https?://\\S+)" 21 | // }, 22 | "env": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | }, 25 | "sourceFileMap": { 26 | "/Views": "${workspaceFolder}/Views" 27 | } 28 | }, 29 | { 30 | "name": ".NET Core Attach", 31 | "type": "coreclr", 32 | "request": "attach" 33 | } 34 | ] 35 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/MaaDownloadServer/MaaDownloadServer.csproj", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "publish", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "publish", 22 | "${workspaceFolder}/MaaDownloadServer/MaaDownloadServer.csproj", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "--project", 36 | "${workspaceFolder}/MaaDownloadServer/MaaDownloadServer.csproj" 37 | ], 38 | "problemMatcher": "$msCompile" 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alisaqaq/maa-download-server-runtime-environment 2 | WORKDIR /app 3 | 4 | COPY ./publish/release /app/ 5 | 6 | RUN ["mkdir", "/app/data"] 7 | 8 | ENV DOTNET_RUNNING_IN_CONTAINER=true 9 | ENV ASPNETCORE_URLS=http://+:80 10 | VOLUME /app/data 11 | EXPOSE 80/tcp 12 | 13 | ENTRYPOINT ["dotnet", "MaaDownloadServer.dll"] 14 | -------------------------------------------------------------------------------- /MaaDownloadServer.Build/BuildContext.cs: -------------------------------------------------------------------------------- 1 | using Cake.Common; 2 | using Cake.Common.Tools.DotNet.MSBuild; 3 | using Cake.Common.Tools.DotNetCore.MSBuild; 4 | using Cake.Core; 5 | using Cake.Frosting; 6 | using LogLevel = Cake.Core.Diagnostics.LogLevel; 7 | using Verbosity = Cake.Core.Diagnostics.Verbosity; 8 | 9 | namespace MaaDownloadServer.Build; 10 | 11 | public class BuildContext : FrostingContext 12 | { 13 | private string Version { get; set; } 14 | 15 | public string MsBuildConfiguration { get; set; } 16 | public string PublishRid { get; set; } 17 | public string Framework { get; set; } 18 | public string Docker { get; set; } 19 | public string DockerArches { get; set; } 20 | public DotNetMSBuildSettings BuildSettings { get; set; } 21 | 22 | public BuildContext(ICakeContext context) : base(context) 23 | { 24 | context.Log.Write(Verbosity.Normal, LogLevel.Information, ""); 25 | MsBuildConfiguration = context.Argument("configuration", "Release"); 26 | Version = context.Argument("maads-version", "0.0.0"); 27 | PublishRid = context.Argument("rid", "portable"); 28 | Framework = context.Argument("framework", "net6.0"); 29 | Docker = context.Argument("docker", "false"); 30 | DockerArches = context.Argument("docker-arches", "amd64,arm64,arm/v7"); 31 | 32 | var versionOk = SemVersion.TryParse(this.Version, out var version); 33 | if (versionOk is false) 34 | { 35 | throw new ArgumentException("Version string is not valid."); 36 | } 37 | 38 | BuildSettings = new DotNetMSBuildSettings() 39 | .TreatAllWarningsAs(MSBuildTreatAllWarningsAs.Error) 40 | .SetVersion(version.AssemblyVersion.ToString()) 41 | .SetFileVersion(version.AssemblyVersion.ToString()) 42 | .SetInformationalVersion(version.VersionString) 43 | .SetAssemblyVersion(version.AssemblyVersion.ToString()); 44 | if (version.IsPreRelease) 45 | { 46 | BuildSettings.SetVersionSuffix(version.PreRelease); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /MaaDownloadServer.Build/MaaDownloadServer.Build.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /MaaDownloadServer.Build/Program.cs: -------------------------------------------------------------------------------- 1 | using Cake.Frosting; 2 | using MaaDownloadServer.Build; 3 | 4 | var sArgs = new List(); 5 | foreach (var arg in args) 6 | { 7 | var sa = arg.Split(" ") 8 | .ToList(); 9 | sa.RemoveAll(x => string.IsNullOrEmpty(x) || string.IsNullOrWhiteSpace(x)); 10 | sArgs.AddRange(sa); 11 | } 12 | 13 | return new CakeHost() 14 | .UseContext() 15 | .Run(sArgs); 16 | -------------------------------------------------------------------------------- /MaaDownloadServer.Build/Tasks/BuildTask.cs: -------------------------------------------------------------------------------- 1 | using Cake.Common; 2 | using Cake.Common.Tools.DotNet; 3 | using Cake.Common.Tools.DotNet.Build; 4 | using Cake.Frosting; 5 | 6 | namespace MaaDownloadServer.Build.Tasks; 7 | 8 | [TaskName("Build")] 9 | [IsDependentOn(typeof(CleanTask))] 10 | public sealed class BuildTask : FrostingTask 11 | { 12 | public override void Run(BuildContext context) 13 | { 14 | context.DotNetBuild("../MaaDownloadServer/MaaDownloadServer.csproj", new DotNetBuildSettings 15 | { 16 | Configuration = context.MsBuildConfiguration, 17 | NoIncremental = context.HasArgument("rebuild"), 18 | Framework = context.Framework, 19 | MSBuildSettings = context.BuildSettings 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /MaaDownloadServer.Build/Tasks/CleanTask.cs: -------------------------------------------------------------------------------- 1 | using Cake.Common.IO; 2 | using Cake.Frosting; 3 | 4 | namespace MaaDownloadServer.Build.Tasks; 5 | 6 | [TaskName("Clean")] 7 | [IsDependentOn(typeof(LoggingTask))] 8 | public sealed class CleanTask : FrostingTask 9 | { 10 | public override void Run(BuildContext context) 11 | { 12 | context.CleanDirectory($"../MaaDownloadServer/bin/{context.MsBuildConfiguration}"); 13 | context.CleanDirectory("../publish"); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /MaaDownloadServer.Build/Tasks/DefaultTask.cs: -------------------------------------------------------------------------------- 1 | using Cake.Frosting; 2 | 3 | namespace MaaDownloadServer.Build.Tasks; 4 | 5 | [TaskName("Default")] 6 | [IsDependentOn(typeof(PostPublishTask))] 7 | public sealed class DefaultTask : FrostingTask { } 8 | -------------------------------------------------------------------------------- /MaaDownloadServer.Build/Tasks/LoggingTask.cs: -------------------------------------------------------------------------------- 1 | using Cake.Frosting; 2 | using LogLevel = Cake.Core.Diagnostics.LogLevel; 3 | using Verbosity = Cake.Core.Diagnostics.Verbosity; 4 | 5 | namespace MaaDownloadServer.Build.Tasks; 6 | 7 | [TaskName("Logging")] 8 | public class LoggingTask : FrostingTask 9 | { 10 | public override void Run(BuildContext context) 11 | { 12 | if (context.Docker is "false") 13 | { 14 | context.Log.Write(Verbosity.Normal, LogLevel.Information, "Build MaaDownloadServer for bare-metal."); 15 | context.Log.Write(Verbosity.Normal, LogLevel.Information, $"Configuration: {context.MsBuildConfiguration}"); 16 | context.Log.Write(Verbosity.Normal, LogLevel.Information, $"Platform: {context.PublishRid}"); 17 | context.Log.Write(Verbosity.Normal, LogLevel.Information, $"Framework: {context.Framework}"); 18 | } 19 | else 20 | { 21 | context.Log.Write(Verbosity.Normal, LogLevel.Information, "Build MaaDownloadServer for Docker."); 22 | context.Log.Write(Verbosity.Normal, LogLevel.Information, $"Configuration: {context.MsBuildConfiguration}"); 23 | context.Log.Write(Verbosity.Normal, LogLevel.Information, $"Docker Arches: {context.DockerArches}"); 24 | context.Log.Write(Verbosity.Normal, LogLevel.Information, $"Framework: {context.Framework}"); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /MaaDownloadServer.Build/Tasks/PostPublishTask.cs: -------------------------------------------------------------------------------- 1 | using System.IO.Compression; 2 | using Cake.Frosting; 3 | 4 | namespace MaaDownloadServer.Build.Tasks; 5 | 6 | [TaskName("PostPublish")] 7 | [IsDependentOn(typeof(PublishTask))] 8 | public class PostPublishTask : FrostingTask 9 | { 10 | public override void Run(BuildContext context) 11 | { 12 | if (context.Docker is "false") 13 | { 14 | if (File.Exists( 15 | $"../publish/{context.Framework}-{context.PublishRid}-{context.MsBuildConfiguration}/appsettings.Development.json")) 16 | { 17 | File.Delete( 18 | $"../publish/{context.Framework}-{context.PublishRid}-{context.MsBuildConfiguration}/appsettings.Development.json"); 19 | } 20 | 21 | ZipFile.CreateFromDirectory( 22 | $"../publish/{context.Framework}-{context.PublishRid}-{context.MsBuildConfiguration}", 23 | $"../publish/MaaDownloadServer-{context.MsBuildConfiguration}-{context.Framework}-{context.PublishRid}.zip"); 24 | } 25 | else 26 | { 27 | var arches = context.DockerArches.Split(","); 28 | foreach (var arch in arches) 29 | { 30 | if (File.Exists( 31 | $"../publish/net6.0-docker-{arch}-{context.MsBuildConfiguration}/appsettings.Development.json")) 32 | { 33 | File.Delete( 34 | $"../publish/net6.0-docker-{arch}-{context.MsBuildConfiguration}/appsettings.Development.json"); 35 | } 36 | 37 | if (File.Exists( 38 | $"../publish/net6.0-docker-{arch}-{context.MsBuildConfiguration}/appsettings.json")) 39 | { 40 | File.Delete( 41 | $"../publish/net6.0-docker-{arch}-{context.MsBuildConfiguration}/appsettings.json"); 42 | } 43 | 44 | if (File.Exists( 45 | $"../publish/net6.0-docker-{arch}-{context.MsBuildConfiguration}/appsettings.Docker.json")) 46 | { 47 | File.Move($"../publish/net6.0-docker-{arch}-{context.MsBuildConfiguration}/appsettings.Docker.json", 48 | $"../publish/net6.0-docker-{arch}-{context.MsBuildConfiguration}/appsettings.json"); 49 | } 50 | else 51 | { 52 | File.Copy($"../MaaDownloadServer/appsettings.Docker.json", 53 | $"../publish/net6.0-docker-{arch}-{context.MsBuildConfiguration}/appsettings.json"); 54 | } 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /MaaDownloadServer.Build/Tasks/PublishTask.cs: -------------------------------------------------------------------------------- 1 | using Cake.Common.Tools.DotNet; 2 | using Cake.Common.Tools.DotNet.Publish; 3 | using Cake.Core.Diagnostics; 4 | using Cake.Frosting; 5 | 6 | namespace MaaDownloadServer.Build.Tasks; 7 | 8 | [TaskName("Publish")] 9 | [IsDependentOn(typeof(BuildTask))] 10 | public sealed class PublishTask : FrostingTask 11 | { 12 | public override void Run(BuildContext context) 13 | { 14 | if (context.Docker is "false") 15 | { 16 | context.DotNetPublish("../MaaDownloadServer/MaaDownloadServer.csproj", new DotNetPublishSettings 17 | { 18 | Configuration = context.MsBuildConfiguration, 19 | SelfContained = false, 20 | OutputDirectory = $"../publish/{context.Framework}-{context.PublishRid}-{context.MsBuildConfiguration}", 21 | Framework = context.Framework, 22 | Runtime = context.PublishRid is "portable" ? null : context.PublishRid, 23 | MSBuildSettings = context.BuildSettings 24 | }); 25 | } 26 | else 27 | { 28 | var arches = context.DockerArches.Split(","); 29 | foreach (var arch in arches) 30 | { 31 | var clrArch = arch switch 32 | { 33 | "amd64" => "x64", 34 | "arm64" => "arm64", 35 | "arm/v7" => "arm", 36 | _ => "?" 37 | }; 38 | if (clrArch is "?") 39 | { 40 | context.Log.Write(Verbosity.Normal, LogLevel.Error, $"Unsupported arch: {arch}"); 41 | continue; 42 | } 43 | context.Log.Write(Verbosity.Normal, LogLevel.Information, $"Publish app for Docker with arch {clrArch}"); 44 | context.DotNetPublish("../MaaDownloadServer/MaaDownloadServer.csproj", new DotNetPublishSettings 45 | { 46 | Configuration = context.MsBuildConfiguration, 47 | SelfContained = false, 48 | OutputDirectory = $"../publish/net6.0-docker-{arch}-{context.MsBuildConfiguration}", 49 | Framework = "net6.0", 50 | Runtime = $"linux-{clrArch}", 51 | MSBuildSettings = context.BuildSettings 52 | }); 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /MaaDownloadServer.LocalTest/.gitignore: -------------------------------------------------------------------------------- 1 | *.cs 2 | -------------------------------------------------------------------------------- /MaaDownloadServer.LocalTest/MaaDownloadServer.LocalTest.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | disable 7 | Exe 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /MaaDownloadServer.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MaaDownloadServer", "MaaDownloadServer\MaaDownloadServer.csproj", "{D38E94E1-32EA-4B55-803C-7ADD569CFEA5}" 4 | EndProject 5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MaaDownloadServer.Build", "MaaDownloadServer.Build\MaaDownloadServer.Build.csproj", "{6FE2B3D1-3F2B-4ED7-A6F9-8E948795F196}" 6 | EndProject 7 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MaaDownloadServer.LocalTest", "MaaDownloadServer.LocalTest\MaaDownloadServer.LocalTest.csproj", "{6AEDA3ED-E6DB-4BD9-BE00-45ED20C1B92D}" 8 | EndProject 9 | Global 10 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 11 | Debug|Any CPU = Debug|Any CPU 12 | Release|Any CPU = Release|Any CPU 13 | EndGlobalSection 14 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 15 | {D38E94E1-32EA-4B55-803C-7ADD569CFEA5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 16 | {D38E94E1-32EA-4B55-803C-7ADD569CFEA5}.Debug|Any CPU.Build.0 = Debug|Any CPU 17 | {D38E94E1-32EA-4B55-803C-7ADD569CFEA5}.Release|Any CPU.ActiveCfg = Release|Any CPU 18 | {D38E94E1-32EA-4B55-803C-7ADD569CFEA5}.Release|Any CPU.Build.0 = Release|Any CPU 19 | {6FE2B3D1-3F2B-4ED7-A6F9-8E948795F196}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 20 | {6FE2B3D1-3F2B-4ED7-A6F9-8E948795F196}.Debug|Any CPU.Build.0 = Debug|Any CPU 21 | {6FE2B3D1-3F2B-4ED7-A6F9-8E948795F196}.Release|Any CPU.ActiveCfg = Release|Any CPU 22 | {6FE2B3D1-3F2B-4ED7-A6F9-8E948795F196}.Release|Any CPU.Build.0 = Release|Any CPU 23 | {6AEDA3ED-E6DB-4BD9-BE00-45ED20C1B92D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {6AEDA3ED-E6DB-4BD9-BE00-45ED20C1B92D}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {6AEDA3ED-E6DB-4BD9-BE00-45ED20C1B92D}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {6AEDA3ED-E6DB-4BD9-BE00-45ED20C1B92D}.Release|Any CPU.Build.0 = Release|Any CPU 27 | EndGlobalSection 28 | EndGlobal 29 | -------------------------------------------------------------------------------- /MaaDownloadServer/Controller/AnnounceController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace MaaDownloadServer.Controller; 4 | 5 | [ApiController] 6 | [Route("announce")] 7 | [ResponseCache(Duration = 0, NoStore = true, Location = ResponseCacheLocation.None)] 8 | public class AnnounceController : ControllerBase 9 | { 10 | private readonly MaaDownloadServerDbContext _dbContext; 11 | 12 | public AnnounceController(MaaDownloadServerDbContext dbContext) 13 | { 14 | _dbContext = dbContext; 15 | } 16 | 17 | [HttpGet] 18 | public ActionResult GetAnnounce([FromQuery] string issuer) 19 | { 20 | if (string.IsNullOrEmpty(issuer)) 21 | { 22 | return NotFound(); 23 | } 24 | 25 | var announceCacheObj = _dbContext.DatabaseCaches.FirstOrDefault(x => x.QueryId == $"persist_anno_{issuer}"); 26 | if (announceCacheObj is null) 27 | { 28 | return NotFound(); 29 | } 30 | 31 | return announceCacheObj.Value; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /MaaDownloadServer/Controller/ComponentController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace MaaDownloadServer.Controller; 4 | 5 | [ApiController] 6 | [Route("component")] 7 | public class ComponentController : ControllerBase 8 | { 9 | private readonly IComponentService _componentService; 10 | 11 | public ComponentController(IComponentService componentService) 12 | { 13 | _componentService = componentService; 14 | } 15 | 16 | [HttpGet("getAll")] 17 | public async Task>> GetComponents() 18 | { 19 | var dtos = await _componentService.GetAllComponents(); 20 | return Ok(dtos); 21 | } 22 | 23 | [HttpGet("getInfo")] 24 | public async Task> GetComponentDetail([FromQuery] string component, 25 | [FromQuery] int page = 1, [FromQuery] int limit = 10) 26 | { 27 | if (page < 1) 28 | { 29 | page = 1; 30 | } 31 | 32 | if (limit < 1) 33 | { 34 | limit = 10; 35 | } 36 | 37 | var dto = await _componentService.GetComponentDetail(component, limit, page); 38 | if (dto is null) 39 | { 40 | return NotFound(); 41 | } 42 | return Ok(dto); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /MaaDownloadServer/Controller/DownloadController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace MaaDownloadServer.Controller; 4 | 5 | [ApiController] 6 | [Route("download/{platform}/{arch}")] 7 | public class DownloadController : ControllerBase 8 | { 9 | private readonly ILogger _logger; 10 | private readonly IDownloadService _downloadService; 11 | private readonly IVersionService _versionService; 12 | private readonly IConfiguration _configuration; 13 | 14 | public DownloadController(ILogger logger, IDownloadService downloadService, IConfiguration configuration, IVersionService versionService) 15 | { 16 | _logger = logger; 17 | _downloadService = downloadService; 18 | _configuration = configuration; 19 | _versionService = versionService; 20 | } 21 | 22 | [HttpGet("{version}")] 23 | public async Task> GetFullPackageDownloadUrl(string platform, string arch, 24 | string version, [FromQuery] string component) 25 | { 26 | var pf = platform.ParseToPlatform(); 27 | var a = arch.ParseToArchitecture(); 28 | if (pf is Platform.UnSupported || a is Architecture.UnSupported) 29 | { 30 | _logger.LogWarning("传入 Platform 值 {Platform} 或 Arch 值 {Arch} 解析为不受支持", platform, arch); 31 | return NotFound(); 32 | } 33 | 34 | PublicContent pc; 35 | 36 | string realVersion; 37 | if (version is "latest") 38 | { 39 | var latestVersion = await GetLatestVersion(component, pf, a); 40 | 41 | if (latestVersion is null) 42 | { 43 | return NotFound(); 44 | } 45 | 46 | pc = await _downloadService.GetFullPackage(component, pf, a, latestVersion.Version.ParseToSemVer()); 47 | realVersion = latestVersion.Version; 48 | } 49 | else 50 | { 51 | var semVerParsed = version.TryParseToSemVer(out var semVer); 52 | if (semVerParsed is false) 53 | { 54 | _logger.LogWarning("传入 version 值 {Version} 解析失败", version); 55 | return NotFound(); 56 | } 57 | 58 | pc = await _downloadService.GetFullPackage(component, pf, a, semVer); 59 | realVersion = version; 60 | } 61 | 62 | if (pc is null) 63 | { 64 | return NotFound(); 65 | } 66 | var dUrl = $"{_configuration["MaaServer:Server:ApiFullUrl"]}/files/{pc.Id}.{pc.FileExtension}"; 67 | var dto = new GetDownloadUrlDto(platform, arch, realVersion, dUrl, pc.Hash); 68 | return Ok(dto); 69 | } 70 | 71 | [HttpGet] 72 | public async Task> GetUpdatePackageDownloadUrl( 73 | string platform, string arch, [FromQuery] string from, [FromQuery] string to, [FromQuery] string component) 74 | { 75 | var pf = platform.ParseToPlatform(); 76 | var a = arch.ParseToArchitecture(); 77 | if (pf is Platform.UnSupported || a is Architecture.UnSupported) 78 | { 79 | _logger.LogWarning("传入 Platform 值 {Platform} 或 Arch 值 {Arch} 解析为不受支持", platform, arch); 80 | return NotFound(); 81 | } 82 | 83 | string realTo; 84 | if (to == "latest") 85 | { 86 | var latestVersion = await GetLatestVersion(component, pf, a); 87 | if (latestVersion is null) 88 | { 89 | return null; 90 | } 91 | 92 | realTo = latestVersion.Version; 93 | } 94 | else 95 | { 96 | realTo = to; 97 | } 98 | 99 | var semVerParsed1 = from.TryParseToSemVer(out var fromSemVer); 100 | var semVerParsed2 = realTo.TryParseToSemVer(out var toSemVer); 101 | if (semVerParsed1 is false || semVerParsed2 is false) 102 | { 103 | _logger.LogWarning("传入 version 值 {From} 或 {To} 解析失败", from, to); 104 | return NotFound(); 105 | } 106 | var pc = await _downloadService.GetUpdatePackage(component, pf, a, fromSemVer, toSemVer); 107 | if (pc is null) 108 | { 109 | return NotFound(); 110 | } 111 | var dUrl = $"{_configuration["MaaServer:Server:ApiFullUrl"]}/files/{pc.Id}.{pc.FileExtension}"; 112 | var dto = new GetDownloadUrlDto(platform, arch, $"{from} -> {realTo}", dUrl, pc.Hash); 113 | return Ok(dto); 114 | } 115 | 116 | private async Task GetLatestVersion(string component, Platform pf, Architecture a) 117 | { 118 | return await _versionService.GetLatestVersion(component, pf, a); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /MaaDownloadServer/Controller/ListController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace MaaDownloadServer.Controller; 4 | 5 | [ApiController] 6 | [Route("list")] 7 | public class ListController : ControllerBase 8 | { 9 | private readonly DirectoryInfo _staticDirectory; 10 | 11 | public ListController(IConfiguration configuration) 12 | { 13 | _staticDirectory = new DirectoryInfo(Path.Combine(configuration["MaaServer:DataDirectories:RootPath"], 14 | configuration["MaaServer:DataDirectories:SubDirectories:Static"])); 15 | } 16 | 17 | [HttpGet("static")] 18 | public ActionResult> GetStaticFileList() 19 | { 20 | var files = _staticDirectory.GetFiles("*", SearchOption.AllDirectories); 21 | var rPaths = files.Select(x => x.FullName.Replace(_staticDirectory.FullName, "")); 22 | return Ok(rPaths); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /MaaDownloadServer/Controller/VersionController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace MaaDownloadServer.Controller; 4 | 5 | [ApiController] 6 | [Route("version")] 7 | public class VersionController : ControllerBase 8 | { 9 | private readonly IVersionService _versionService; 10 | private readonly ILogger _logger; 11 | 12 | public VersionController(IVersionService versionService, ILogger logger) 13 | { 14 | _versionService = versionService; 15 | _logger = logger; 16 | } 17 | 18 | [HttpGet("{platform}/{arch}/{version}")] 19 | public async Task> GetVersion(string platform, string arch, string version, [FromQuery] string component) 20 | { 21 | var pf = platform.ParseToPlatform(); 22 | var a = arch.ParseToArchitecture(); 23 | if (pf is Platform.UnSupported || a is Architecture.UnSupported) 24 | { 25 | _logger.LogWarning("传入 Platform 值 {Platform} 或 Arch 值 {Arch} 解析为不受支持", platform, arch); 26 | return NotFound(); 27 | } 28 | 29 | Package package; 30 | if (version is "latest") 31 | { 32 | package = await _versionService.GetLatestVersion(component, pf, a); 33 | } 34 | else 35 | { 36 | var semVerParsed = version.TryParseToSemVer(out var semVer); 37 | if (semVerParsed is false) 38 | { 39 | _logger.LogWarning("传入 version 值 {Version} 解析失败", version); 40 | return NotFound(); 41 | } 42 | package = await _versionService.GetVersion(component, pf, a, semVer); 43 | } 44 | 45 | if (package is not null) 46 | { 47 | return Ok(new GetVersionDto(platform, arch, 48 | new VersionDetail(package.Version, package.PublishTime, package.UpdateLog, 49 | package.Resources.Select(x => new ResourceMetadata(x.FileName, x.Path, x.Hash)).ToList()))); 50 | } 51 | 52 | _logger.LogWarning("GetVersion() returned null"); 53 | return NotFound(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /MaaDownloadServer/Database/DbContextExtension.cs: -------------------------------------------------------------------------------- 1 | namespace MaaDownloadServer.Database; 2 | 3 | public static class DbContextExtension 4 | { 5 | public static void AddMaaDownloadServerDbContext(this IServiceCollection serviceCollection) 6 | { 7 | serviceCollection.AddDbContext(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /MaaDownloadServer/Database/MaaDownloadServerDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 3 | 4 | namespace MaaDownloadServer.Database; 5 | 6 | public class MaaDownloadServerDbContext : DbContext 7 | { 8 | private readonly IConfiguration _configuration; 9 | 10 | public MaaDownloadServerDbContext( 11 | DbContextOptions options, 12 | IConfiguration configuration) : base(options) 13 | { 14 | _configuration = configuration; 15 | } 16 | 17 | public DbSet Packages { get; set; } 18 | public DbSet Resources { get; set; } 19 | public DbSet PublicContents { get; set; } 20 | public DbSet DatabaseCaches { get; set; } 21 | public DbSet DownloadCounts { get; set; } 22 | 23 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 24 | { 25 | var connectionString = $"Data Source={Path.Combine(_configuration["MaaServer:DataDirectories:RootPath"], _configuration["MaaServer:DataDirectories:SubDirectories:Database"], "data.db")};"; 26 | optionsBuilder.UseSqlite(connectionString, builder => 27 | builder.MigrationsAssembly("MaaDownloadServer")); 28 | } 29 | 30 | protected override void OnModelCreating(ModelBuilder modelBuilder) 31 | { 32 | #region 转换器 33 | 34 | modelBuilder 35 | .Entity() 36 | .Property(x => x.Architecture) 37 | .HasConversion>(); 38 | 39 | modelBuilder 40 | .Entity() 41 | .Property(x => x.Platform) 42 | .HasConversion>(); 43 | 44 | #endregion 45 | 46 | #region 多对多 47 | 48 | modelBuilder 49 | .Entity() 50 | .HasMany(x => x.Resources); 51 | 52 | modelBuilder 53 | .Entity() 54 | .HasMany(x => x.Packages); 55 | 56 | #endregion 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /MaaDownloadServer/Enums/AnnounceLevel.cs: -------------------------------------------------------------------------------- 1 | namespace MaaDownloadServer.Enums; 2 | 3 | public enum AnnounceLevel 4 | { 5 | Information, 6 | Warning, 7 | Error 8 | } 9 | -------------------------------------------------------------------------------- /MaaDownloadServer/Enums/Architecture.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable InconsistentNaming 2 | namespace MaaDownloadServer.Enums; 3 | 4 | public enum Architecture 5 | { 6 | x64, 7 | arm64, 8 | NoArch, 9 | UnSupported 10 | } 11 | -------------------------------------------------------------------------------- /MaaDownloadServer/Enums/ChecksumType.cs: -------------------------------------------------------------------------------- 1 | namespace MaaDownloadServer.Enums; 2 | 3 | public enum ChecksumType 4 | { 5 | None, 6 | Md5, 7 | Sha1, 8 | Sha256, 9 | Sha384, 10 | Sha512 11 | } 12 | -------------------------------------------------------------------------------- /MaaDownloadServer/Enums/Platform.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable InconsistentNaming 2 | namespace MaaDownloadServer.Enums; 3 | 4 | public enum Platform 5 | { 6 | windows, 7 | linux, 8 | macos, 9 | NoPlatform, 10 | UnSupported 11 | } 12 | -------------------------------------------------------------------------------- /MaaDownloadServer/Enums/ProgramExitCode.cs: -------------------------------------------------------------------------------- 1 | namespace MaaDownloadServer.Enums; 2 | 3 | public struct ProgramExitCode 4 | { 5 | public const int ConfigurationProviderIsNull = -1; 6 | public const int NoPythonInterpreter = -2; 7 | public const int ScriptDoNotHaveConfigFile = -3; 8 | public const int FailedToParseScriptConfigFile = -4; 9 | public const int FailedToCreatePythonVenv = -5; 10 | } 11 | -------------------------------------------------------------------------------- /MaaDownloadServer/Enums/PublicContentTagType.cs: -------------------------------------------------------------------------------- 1 | namespace MaaDownloadServer.Enums; 2 | 3 | public enum PublicContentTagType 4 | { 5 | FullPackage, 6 | UpdatePackage 7 | } 8 | -------------------------------------------------------------------------------- /MaaDownloadServer/Extensions/FileSystemExtension.cs: -------------------------------------------------------------------------------- 1 | namespace MaaDownloadServer.Extensions; 2 | 3 | public static class FileSystemExtension 4 | { 5 | public static void CopyTo(this DirectoryInfo srcPath, string destPath) 6 | { 7 | Directory.CreateDirectory(destPath); 8 | Parallel.ForEach(srcPath.GetDirectories("*", SearchOption.AllDirectories), 9 | srcInfo => Directory.CreateDirectory($"{destPath}{srcInfo.FullName[srcPath.FullName.Length..]}")); 10 | Parallel.ForEach(srcPath.GetFiles("*", SearchOption.AllDirectories), 11 | srcInfo => File.Copy(srcInfo.FullName, $"{destPath}{srcInfo.FullName[srcPath.FullName.Length..]}", true)); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /MaaDownloadServer/Extensions/HttpClientFactoryExtension.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using Polly; 3 | 4 | namespace MaaDownloadServer.Extensions; 5 | 6 | public static class HttpClientFactoryExtension 7 | { 8 | public static void AddHttpClients(this IServiceCollection service, MaaConfigurationProvider provider) 9 | { 10 | var option = provider.GetOption(); 11 | var userAgent = option.Value.UserAgent; 12 | var proxyUrl = option.Value.Proxy; 13 | var version = provider.GetConfiguration().GetValue("AssemblyVersion"); 14 | var proxy = string.IsNullOrEmpty(proxyUrl) ? null : new WebProxy(proxyUrl); 15 | 16 | 17 | service.AddHttpClient("NoProxy", client => 18 | { 19 | client.DefaultRequestHeaders.Add("User-Agent", userAgent); 20 | }) 21 | .ConfigurePrimaryHttpMessageHandler(() => 22 | new HttpClientHandler { Proxy = null, UseProxy = false, AllowAutoRedirect = true }) 23 | .AddTransientHttpErrorPolicy(builder => 24 | builder.WaitAndRetryAsync(3, _ => TimeSpan.FromMilliseconds(2000))); 25 | 26 | service.AddHttpClient("Proxy", client => 27 | { 28 | client.DefaultRequestHeaders.Add("User-Agent", userAgent); 29 | }) 30 | .ConfigurePrimaryHttpMessageHandler(() => 31 | new HttpClientHandler { Proxy = proxy, UseProxy = proxy is not null, AllowAutoRedirect = true }) 32 | .AddTransientHttpErrorPolicy(builder => 33 | builder.WaitAndRetryAsync(3, _ => TimeSpan.FromMilliseconds(2000))); 34 | 35 | service.AddHttpClient("ServerChan", client => 36 | { 37 | client.DefaultRequestHeaders.Add("User-Agent", $"MaaDownloadServer/{version}"); 38 | client.BaseAddress = new Uri("https://sctapi.ftqq.com/"); 39 | }) 40 | .ConfigurePrimaryHttpMessageHandler(() => 41 | new HttpClientHandler { AllowAutoRedirect = true }) 42 | .AddTransientHttpErrorPolicy(builder => 43 | builder.WaitAndRetryAsync(3, _ => TimeSpan.FromMilliseconds(1000))); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /MaaDownloadServer/Extensions/OptionExtension.cs: -------------------------------------------------------------------------------- 1 | using AspNetCoreRateLimit; 2 | 3 | namespace MaaDownloadServer.Extensions; 4 | 5 | public static class OptionExtension 6 | { 7 | public static void AddMaaOptions(this IServiceCollection service, MaaConfigurationProvider provider) 8 | { 9 | service.AddOptions(); 10 | 11 | service.Configure(provider.GetConfigurationSection("IpRateLimiting")); 12 | service.Configure(provider.GetConfigurationSection("IpRateLimitPolicies")); 13 | 14 | service.AddConfigureOption(provider); 15 | service.AddConfigureOption(provider); 16 | service.AddConfigureOption(provider); 17 | service.AddConfigureOption(provider); 18 | service.AddConfigureOption(provider); 19 | service.AddConfigureOption(provider); 20 | } 21 | 22 | private static void AddConfigureOption(this IServiceCollection service, MaaConfigurationProvider provider) 23 | where T : class, IMaaOption, new() 24 | { 25 | service.Configure(provider.GetOptionConfigurationSection()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /MaaDownloadServer/Extensions/PlatformArchExtension.cs: -------------------------------------------------------------------------------- 1 | namespace MaaDownloadServer.Extensions; 2 | 3 | public static class PlatformArchExtension 4 | { 5 | public static Platform ParseToPlatform(this string platformString) 6 | { 7 | var str = platformString.ToLower(); 8 | return str switch 9 | { 10 | "windows" => Platform.windows, 11 | "win" => Platform.windows, 12 | "linux" => Platform.linux, 13 | "macos" => Platform.macos, 14 | "osx" => Platform.macos, 15 | "no_platform" => Platform.NoPlatform, 16 | _ => Platform.UnSupported 17 | }; 18 | } 19 | 20 | public static Architecture ParseToArchitecture(this string architectureString) 21 | { 22 | var str = architectureString.ToLower(); 23 | return str switch 24 | { 25 | "x64" => Architecture.x64, 26 | "arm64" => Architecture.arm64, 27 | "no_arch" => Architecture.NoArch, 28 | _ => Architecture.UnSupported 29 | }; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /MaaDownloadServer/Extensions/SemanticVersionExtension.cs: -------------------------------------------------------------------------------- 1 | using Semver; 2 | 3 | namespace MaaDownloadServer.Extensions; 4 | 5 | public static class SemanticVersionExtension 6 | { 7 | public static SemVersion ParseToSemVer(this string semverString) 8 | { 9 | return SemVersion.Parse(semverString, SemVersionStyles.Strict); 10 | } 11 | 12 | public static bool TryParseToSemVer(this string semverString, out SemVersion semVersion) 13 | { 14 | return SemVersion.TryParse(semverString, SemVersionStyles.Strict, out semVersion); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /MaaDownloadServer/Extensions/ServiceExtension.cs: -------------------------------------------------------------------------------- 1 | using MaaDownloadServer.Services.Base; 2 | using MaaDownloadServer.Services.Controller; 3 | 4 | namespace MaaDownloadServer.Extensions; 5 | 6 | public static class ServiceExtension 7 | { 8 | public static void AddMaaServices(this IServiceCollection serviceCollection) 9 | { 10 | // Scoped 11 | serviceCollection.AddScoped(); 12 | serviceCollection.AddScoped(); 13 | serviceCollection.AddScoped(); 14 | 15 | // Controller 16 | serviceCollection.AddScoped(); 17 | serviceCollection.AddScoped(); 18 | serviceCollection.AddScoped(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /MaaDownloadServer/External/Python.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using EscapeRoute; 3 | 4 | namespace MaaDownloadServer.External; 5 | 6 | public class Python 7 | { 8 | public static bool EnvironmentCheck(ILogger logger, string pythonExecutable) 9 | { 10 | // 检查 Python 是否存在 11 | try 12 | { 13 | var pythonVersionProcessStartInfo = new ProcessStartInfo(pythonExecutable, "--version") 14 | { 15 | RedirectStandardOutput = true, 16 | RedirectStandardError = true 17 | }; 18 | logger.LogInformation("运行 {Cmd}", pythonVersionProcessStartInfo.FileName + " " + pythonVersionProcessStartInfo.Arguments); 19 | var pythonVersionProcess = Process.Start(pythonVersionProcessStartInfo); 20 | if (pythonVersionProcess is null) 21 | { 22 | logger.LogCritical("Python 环境检查失败,无法启动 Python 进程"); 23 | return false; 24 | } 25 | var standardError = pythonVersionProcess.StandardError.ReadToEnd(); 26 | var standardOutput = pythonVersionProcess.StandardOutput.ReadToEnd(); 27 | if (standardError is not "") 28 | { 29 | logger.LogCritical("Python 环境检查失败,错误信息:{Err}", standardError); 30 | return false; 31 | } 32 | logger.LogInformation("Python 环境检查成功,版本:{Out}", standardOutput); 33 | } 34 | catch (Exception e) 35 | { 36 | logger.LogCritical(e, "检查 Python 环境时出现异常"); 37 | return false; 38 | } 39 | 40 | // 检查 Python 是否存在 Pip 41 | try 42 | { 43 | var pipProcessStartInfo = new ProcessStartInfo(pythonExecutable, "-m pip --version") 44 | { 45 | RedirectStandardOutput = true, 46 | RedirectStandardError = true 47 | }; 48 | logger.LogInformation("运行 {Cmd}", pipProcessStartInfo.FileName + " " + pipProcessStartInfo.Arguments); 49 | var pipProcess = Process.Start(pipProcessStartInfo); 50 | if (pipProcess is null) 51 | { 52 | logger.LogCritical("Python Pip 环境检查失败,无法启动 pip 进程"); 53 | return false; 54 | } 55 | 56 | var standardError = pipProcess.StandardError.ReadToEnd(); 57 | var standardOutput = pipProcess.StandardOutput.ReadToEnd(); 58 | if (standardError is not "") 59 | { 60 | logger.LogCritical("Python Pip 环境检查失败,错误信息:{Err}", standardError); 61 | return false; 62 | } 63 | 64 | logger.LogInformation("Python Pip 环境检查成功,pip 版本:{Out}", standardOutput); 65 | } 66 | catch (Exception e) 67 | { 68 | logger.LogCritical(e, "检查 Python Pip 环境时出现异常"); 69 | return false; 70 | } 71 | 72 | // 检查是否安装了 virtualenv 73 | try 74 | { 75 | var virtualenvProcessStartInfo = new ProcessStartInfo(pythonExecutable, "-m pip show virtualenv") 76 | { 77 | RedirectStandardOutput = true, 78 | RedirectStandardError = true 79 | }; 80 | logger.LogInformation("运行 {Cmd}", virtualenvProcessStartInfo.FileName + " " + virtualenvProcessStartInfo.Arguments); 81 | var virtualenvProcess = Process.Start(virtualenvProcessStartInfo); 82 | if (virtualenvProcess is null) 83 | { 84 | logger.LogCritical("Python virtualenv 环境检查失败,无法启动 virtualenv 进程"); 85 | return false; 86 | } 87 | 88 | var standardError = virtualenvProcess.StandardError.ReadToEnd(); 89 | var standardOutput = virtualenvProcess.StandardOutput.ReadToEnd(); 90 | if (standardError is not "") 91 | { 92 | logger.LogCritical("Python virtualenv 环境检查失败,错误信息:{Err}", standardError); 93 | return false; 94 | } 95 | 96 | logger.LogInformation("Python virtualenv 环境检查成功,virtualenv 版本:{Out}", standardOutput); 97 | } 98 | catch (Exception e) 99 | { 100 | logger.LogCritical(e, "检查 Python virtualenv 环境时出现异常"); 101 | return false; 102 | } 103 | 104 | return true; 105 | } 106 | 107 | public static bool CreateVirtualEnvironment(ILogger logger, string pythonExecutable, string virtualenvPath, string requirements) 108 | { 109 | if (Directory.Exists(virtualenvPath)) 110 | { 111 | var bin = Path.Combine(virtualenvPath, "bin"); 112 | if (Directory.Exists(bin)) 113 | { 114 | var pythonExist = Directory.GetFiles(bin, "python*").Any(); 115 | if (pythonExist) 116 | { 117 | logger.LogWarning("已存在的 Python 虚拟环境:{Path}", virtualenvPath); 118 | return true; 119 | } 120 | } 121 | } 122 | 123 | try 124 | { 125 | var virtualenvProcessStartInfo = new ProcessStartInfo(pythonExecutable, $"-m venv {virtualenvPath}") 126 | { 127 | RedirectStandardError = true 128 | }; 129 | logger.LogInformation("运行 {Cmd}", virtualenvProcessStartInfo.FileName + " " + virtualenvProcessStartInfo.Arguments); 130 | var virtualenvProcess = Process.Start(virtualenvProcessStartInfo); 131 | if (virtualenvProcess is null) 132 | { 133 | logger.LogCritical("Python virtualenv 创建失败,无法启动 virtualenv 进程"); 134 | if (Directory.Exists(virtualenvPath)) 135 | { 136 | Directory.Delete(virtualenvPath, true); 137 | } 138 | return false; 139 | } 140 | 141 | var standardError = virtualenvProcess.StandardError.ReadToEnd(); 142 | if (standardError is not "") 143 | { 144 | logger.LogCritical("Python virtualenv 创建失败,错误信息:{Err}", standardError); 145 | if (Directory.Exists(virtualenvPath)) 146 | { 147 | Directory.Delete(virtualenvPath, true); 148 | } 149 | return false; 150 | } 151 | 152 | if (requirements is null) 153 | { 154 | logger.LogInformation("Python virtualenv 在 {Path} 创建成功,无依赖项", virtualenvPath); 155 | return true; 156 | } 157 | 158 | Debug.Assert(virtualenvPath != null, nameof(virtualenvPath) + " != null"); 159 | var pipProcessStartInfo = new ProcessStartInfo(Path.Combine(virtualenvPath, "bin", "pip"), $"install -r {requirements}") 160 | { 161 | RedirectStandardError = true 162 | }; 163 | logger.LogInformation("运行 {Cmd}", pipProcessStartInfo.FileName + " " + pipProcessStartInfo.Arguments); 164 | var pipProcess = Process.Start(pipProcessStartInfo); 165 | if (pipProcess is null) 166 | { 167 | logger.LogCritical("Python 依赖项安装失败,无法启动 pip 进程"); 168 | if (Directory.Exists(virtualenvPath)) 169 | { 170 | Directory.Delete(virtualenvPath, true); 171 | } 172 | return false; 173 | } 174 | 175 | standardError = pipProcess.StandardError.ReadToEnd(); 176 | if (standardError is not "") 177 | { 178 | logger.LogCritical("Python 依赖项安装失败,错误信息:{Err}", standardError); 179 | if (Directory.Exists(virtualenvPath)) 180 | { 181 | Directory.Delete(virtualenvPath, true); 182 | } 183 | return false; 184 | } 185 | 186 | logger.LogInformation("Python 依赖项安装成功"); 187 | } 188 | catch (Exception e) 189 | { 190 | logger.LogCritical(e, "创建 Python virtualenv 环境时出现异常"); 191 | return false; 192 | } 193 | 194 | return true; 195 | } 196 | 197 | public static string Run(ILogger logger, string pythonExecutable, string scriptFile, IEnumerable args) 198 | { 199 | try 200 | { 201 | var escapeRoute = new EscapeRouter(); 202 | var formattedArgs = args 203 | .Select(x => x.Replace("\r\n", "").Replace("\n", "").Replace(" ", "")) 204 | .Select(x => escapeRoute.ParseAsync(x).Result) 205 | .ToArray(); 206 | 207 | var pythonStartInfo = new ProcessStartInfo(pythonExecutable, $"{scriptFile} {string.Join(" ", formattedArgs)}") 208 | { 209 | RedirectStandardError = true, 210 | RedirectStandardOutput = true, 211 | UseShellExecute = false 212 | }; 213 | logger.LogDebug("运行 {Cmd}", pythonStartInfo.FileName + " " + pythonStartInfo.Arguments); 214 | var pythonProcess = Process.Start(pythonStartInfo); 215 | if (pythonProcess is null) 216 | { 217 | logger.LogCritical("Python 脚本运行失败,无法启动 Python 进程"); 218 | return null; 219 | } 220 | 221 | var standardOutput = pythonProcess.StandardOutput.ReadToEnd(); 222 | var standardError = pythonProcess.StandardError.ReadToEnd(); 223 | 224 | if (standardError is not "") 225 | { 226 | logger.LogError("Python 脚本运行失败,错误信息:{Err}", standardError); 227 | } 228 | 229 | return standardOutput; 230 | } 231 | catch (Exception e) 232 | { 233 | logger.LogCritical(e, "运行 Python 脚本时出现异常"); 234 | return null; 235 | } 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /MaaDownloadServer/Global.cs: -------------------------------------------------------------------------------- 1 | global using Microsoft.Extensions.Logging; 2 | global using MaaDownloadServer.Services.Base.Interfaces; 3 | global using MaaDownloadServer.Services.Controller.Interfaces; 4 | global using MaaDownloadServer.Enums; 5 | global using MaaDownloadServer.Utils; 6 | global using MaaDownloadServer.Model.Entities; 7 | global using MaaDownloadServer.Model.General; 8 | global using MaaDownloadServer.Model.Dto; 9 | global using MaaDownloadServer.Model.External; 10 | global using MaaDownloadServer.Model.Attributes; 11 | global using MaaDownloadServer.Model.Options; 12 | global using MaaDownloadServer.Database; 13 | global using MaaDownloadServer.Extensions; 14 | global using MaaDownloadServer.Providers; 15 | -------------------------------------------------------------------------------- /MaaDownloadServer/Jobs/JobExtension.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Options; 2 | using Quartz; 3 | 4 | namespace MaaDownloadServer.Jobs; 5 | 6 | public static class JobExtension 7 | { 8 | public static void AddQuartzJobs( 9 | this IServiceCollection serviceCollection, 10 | IOptions option, 11 | List componentConfigurations) 12 | { 13 | serviceCollection.AddQuartz(q => 14 | { 15 | q.SchedulerId = "MaaServer-Download-Main-Scheduler"; 16 | q.SchedulerName = "MaaDownloadServer Main Scheduler"; 17 | q.UseMicrosoftDependencyInjectionJobFactory(); 18 | q.UseSimpleTypeLoader(); 19 | q.UseInMemoryStore(); 20 | q.UseDefaultThreadPool(10); 21 | 22 | // 组件更新任务 23 | var componentCount = 1; 24 | foreach (var componentConfiguration in componentConfigurations) 25 | { 26 | var startDelay = 0 + componentCount * 0.5; 27 | q.ScheduleJob(trigger => 28 | { 29 | trigger.WithIdentity($"Package-{componentConfiguration.Name}-Update-Trigger", "Package-Update-Trigger") 30 | .WithCalendarIntervalSchedule(schedule => 31 | { 32 | schedule.WithIntervalInMinutes(componentConfiguration.Interval); 33 | schedule.InTimeZone(TimeZoneInfo.Local); 34 | schedule.WithMisfireHandlingInstructionDoNothing(); 35 | }) 36 | .StartAt(DateTimeOffset.Now.AddMinutes(startDelay)); 37 | }, job => 38 | { 39 | job.WithIdentity($"Package-{componentConfiguration.Name}-Update-Job", "Package-Update-Job"); 40 | IDictionary data = new Dictionary { { "configuration", componentConfiguration } }; 41 | job.SetJobData(new JobDataMap(data)); 42 | }); 43 | 44 | componentCount++; 45 | } 46 | 47 | // Public Content 过期检查任务 48 | q.ScheduleJob(trigger => 49 | { 50 | trigger.WithIdentity("Public-Content-Check-Trigger", "Database") 51 | .WithCalendarIntervalSchedule(schedule => 52 | { 53 | schedule.WithIntervalInMinutes(option.Value.OutdatedCheckInterval); 54 | schedule.InTimeZone(TimeZoneInfo.Local); 55 | schedule.WithMisfireHandlingInstructionDoNothing(); 56 | }) 57 | .StartAt(DateTimeOffset.Now.AddMinutes(10)); 58 | }, job => 59 | { 60 | job.WithIdentity("Public-Content-Check-Job", "Database"); 61 | }); 62 | }); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /MaaDownloadServer/Jobs/PublicContentCheckJob.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Quartz; 3 | 4 | namespace MaaDownloadServer.Jobs; 5 | 6 | public class PublicContentCheckJob : IJob 7 | { 8 | private readonly ILogger _logger; 9 | private readonly IConfigurationService _configurationService; 10 | private readonly MaaDownloadServerDbContext _dbContext; 11 | 12 | public PublicContentCheckJob( 13 | ILogger logger, 14 | IConfigurationService configurationService, 15 | MaaDownloadServerDbContext dbContext) 16 | { 17 | _logger = logger; 18 | _configurationService = configurationService; 19 | _dbContext = dbContext; 20 | } 21 | 22 | public async Task Execute(IJobExecutionContext context) 23 | { 24 | _logger.LogInformation("开始执行 Public Content 检查任务"); 25 | var now = DateTime.Now; 26 | 27 | var outdatedPublicContents = await _dbContext.PublicContents 28 | .Where(x => x.Duration < now) 29 | .ToListAsync(); 30 | 31 | _logger.LogInformation("找到 {ODCount} 个过期的 Public Content", outdatedPublicContents.Count); 32 | 33 | var pendingRemove = new List(); 34 | foreach (var pc in outdatedPublicContents) 35 | { 36 | try 37 | { 38 | var path = Path.Combine(_configurationService.GetPublicDirectory(), $"{pc.Id}.zip"); 39 | if (File.Exists(path)) 40 | { 41 | File.Delete(path); 42 | pendingRemove.Add(pc); 43 | _logger.LogDebug("删除过期的 Public Content {Id}", pc.Id); 44 | continue; 45 | } 46 | _logger.LogWarning("删除过期文件 ID 为 {ID},但是文件 {Path} 不存在", pc.Id, path); 47 | } 48 | catch (Exception e) 49 | { 50 | _logger.LogError(e, "删除 ID 为 {Id} 的 Public Content 失败", pc.Id); 51 | } 52 | } 53 | 54 | _dbContext.PublicContents.RemoveRange(pendingRemove); 55 | await _dbContext.SaveChangesAsync(); 56 | _logger.LogInformation("成功删除 {RealDeleted}/{AllDeleted} 个过期的 Public Content", 57 | pendingRemove.Count, outdatedPublicContents.Count); 58 | if (pendingRemove.Count != outdatedPublicContents.Count) 59 | { 60 | _logger.LogWarning("未能删除所有过期的 Public Content,删除失败 {Failed} 个", 61 | outdatedPublicContents.Count - pendingRemove.Count); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /MaaDownloadServer/MaaDownloadServer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | disable 6 | enable 7 | embedded 8 | True 9 | AGPL-3.0-or-later 10 | Linux 11 | 12 | 13 | 14 | false 15 | none 16 | 17 | 18 | 19 | true 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | all 29 | runtime; build; native; contentfiles; analyzers; buildtransitive 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /MaaDownloadServer/MaaDownloadServer.csproj.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | True 3 | True 4 | True 5 | True 6 | True 7 | True 8 | True 9 | True 10 | True 11 | True 12 | True 13 | True 14 | True 15 | True 16 | 17 | -------------------------------------------------------------------------------- /MaaDownloadServer/Middleware/DownloadCountMiddleware.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | 3 | namespace MaaDownloadServer.Middleware; 4 | 5 | public class DownloadCountMiddleware 6 | { 7 | 8 | private readonly ILogger _logger; 9 | private readonly RequestDelegate _next; 10 | 11 | public DownloadCountMiddleware(RequestDelegate next, ILogger logger) 12 | { 13 | _next = next; 14 | _logger = logger; 15 | } 16 | 17 | public async Task InvokeAsync(HttpContext context, MaaDownloadServerDbContext dbContext) 18 | { 19 | await _next(context); 20 | 21 | var requestPath = context.Request.Path; 22 | if (requestPath.StartsWithSegments(new PathString("/files")) is false) 23 | { 24 | return; 25 | } 26 | 27 | if (context.Response.StatusCode != StatusCodes.Status200OK) 28 | { 29 | return; 30 | } 31 | 32 | _logger.LogDebug("中间件监测到文件下载请求,路径为 {P}", requestPath); 33 | 34 | var fileName = requestPath.Value?.Replace("/files/", ""); 35 | 36 | if (fileName is null) 37 | { 38 | _logger.LogWarning("下载计数中间件找不到文件或 Path 解析失败,当前 Path:{P}", requestPath); 39 | return; 40 | } 41 | 42 | var fileId = fileName.Split(".")[0]; 43 | 44 | var res = await dbContext.PublicContents 45 | .FirstOrDefaultAsync(x => x.Id == Guid.Parse(fileId)); 46 | 47 | if (res is null) 48 | { 49 | _logger.LogWarning("下载计数中间件找不到文件或 Path 解析失败,当前 Path:{P}", requestPath); 50 | return; 51 | } 52 | 53 | var tag = res.Tag.ParseFromTagString(); 54 | 55 | var existed = await dbContext.DownloadCounts 56 | .FirstOrDefaultAsync(x => 57 | x.ComponentName == tag.Component && 58 | x.FromVersion == tag.Version.ToString() && 59 | (tag.Type == PublicContentTagType.FullPackage || x.ToVersion == tag.Target.ToString())); 60 | 61 | if (existed is not null) 62 | { 63 | existed.Count++; 64 | dbContext.Update(existed); 65 | await dbContext.SaveChangesAsync(); 66 | return; 67 | } 68 | 69 | var item = new DownloadCount 70 | { 71 | Id = Guid.NewGuid(), 72 | ComponentName = tag.Component, 73 | FromVersion = tag.Version.ToString(), 74 | ToVersion = tag.Type is PublicContentTagType.FullPackage ? "" : tag.Target.ToString(), 75 | Count = 1 76 | }; 77 | 78 | await dbContext.DownloadCounts.AddAsync(item); 79 | await dbContext.SaveChangesAsync(); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /MaaDownloadServer/Migrations/20220307161059_FixNameTypo.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace MaaDownloadServer.Migrations 6 | { 7 | public partial class FixNameTypo : Migration 8 | { 9 | protected override void Up(MigrationBuilder migrationBuilder) 10 | { 11 | migrationBuilder.RenameColumn( 12 | name: "jp_name", 13 | table: "ark_penguin_item", 14 | newName: "ja_name"); 15 | } 16 | 17 | protected override void Down(MigrationBuilder migrationBuilder) 18 | { 19 | migrationBuilder.RenameColumn( 20 | name: "ja_name", 21 | table: "ark_penguin_item", 22 | newName: "jp_name"); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /MaaDownloadServer/Migrations/20220316053837_RemoveGameData.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using MaaDownloadServer.Database; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Migrations; 7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 8 | 9 | #nullable disable 10 | 11 | namespace MaaDownloadServer.Migrations 12 | { 13 | [DbContext(typeof(MaaDownloadServerDbContext))] 14 | [Migration("20220316053837_RemoveGameData")] 15 | partial class RemoveGameData 16 | { 17 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 18 | { 19 | #pragma warning disable 612, 618 20 | modelBuilder.HasAnnotation("ProductVersion", "6.0.1"); 21 | 22 | modelBuilder.Entity("MaaDownloadServer.Model.Entities.DatabaseCache", b => 23 | { 24 | b.Property("Id") 25 | .ValueGeneratedOnAdd() 26 | .HasColumnType("TEXT") 27 | .HasColumnName("id"); 28 | 29 | b.Property("QueryId") 30 | .HasColumnType("TEXT") 31 | .HasColumnName("query_id"); 32 | 33 | b.Property("Value") 34 | .HasColumnType("TEXT") 35 | .HasColumnName("value"); 36 | 37 | b.HasKey("Id"); 38 | 39 | b.ToTable("database_cache"); 40 | }); 41 | 42 | modelBuilder.Entity("MaaDownloadServer.Model.Entities.Package", b => 43 | { 44 | b.Property("Id") 45 | .ValueGeneratedOnAdd() 46 | .HasColumnType("TEXT") 47 | .HasColumnName("id"); 48 | 49 | b.Property("Architecture") 50 | .IsRequired() 51 | .HasColumnType("TEXT") 52 | .HasColumnName("architecture"); 53 | 54 | b.Property("Component") 55 | .HasColumnType("TEXT") 56 | .HasColumnName("component"); 57 | 58 | b.Property("Platform") 59 | .IsRequired() 60 | .HasColumnType("TEXT") 61 | .HasColumnName("platform"); 62 | 63 | b.Property("PublishTime") 64 | .HasColumnType("TEXT") 65 | .HasColumnName("publish_time"); 66 | 67 | b.Property("UpdateLog") 68 | .HasColumnType("TEXT") 69 | .HasColumnName("update_log"); 70 | 71 | b.Property("Version") 72 | .HasColumnType("TEXT") 73 | .HasColumnName("version"); 74 | 75 | b.HasKey("Id"); 76 | 77 | b.ToTable("package"); 78 | }); 79 | 80 | modelBuilder.Entity("MaaDownloadServer.Model.Entities.PublicContent", b => 81 | { 82 | b.Property("Id") 83 | .ValueGeneratedOnAdd() 84 | .HasColumnType("TEXT") 85 | .HasColumnName("id"); 86 | 87 | b.Property("AddTime") 88 | .HasColumnType("TEXT") 89 | .HasColumnName("add_time"); 90 | 91 | b.Property("Duration") 92 | .HasColumnType("TEXT") 93 | .HasColumnName("duration"); 94 | 95 | b.Property("FileExtension") 96 | .HasColumnType("TEXT") 97 | .HasColumnName("file_extension"); 98 | 99 | b.Property("Hash") 100 | .HasColumnType("TEXT") 101 | .HasColumnName("hash"); 102 | 103 | b.Property("Tag") 104 | .HasColumnType("TEXT") 105 | .HasColumnName("tag"); 106 | 107 | b.HasKey("Id"); 108 | 109 | b.ToTable("public_content"); 110 | }); 111 | 112 | modelBuilder.Entity("MaaDownloadServer.Model.Entities.Resource", b => 113 | { 114 | b.Property("Id") 115 | .ValueGeneratedOnAdd() 116 | .HasColumnType("TEXT") 117 | .HasColumnName("id"); 118 | 119 | b.Property("FileName") 120 | .HasColumnType("TEXT") 121 | .HasColumnName("file_name"); 122 | 123 | b.Property("Hash") 124 | .HasColumnType("TEXT") 125 | .HasColumnName("hash"); 126 | 127 | b.Property("Path") 128 | .HasColumnType("TEXT") 129 | .HasColumnName("path"); 130 | 131 | b.HasKey("Id"); 132 | 133 | b.ToTable("resource"); 134 | }); 135 | 136 | modelBuilder.Entity("PackageResource", b => 137 | { 138 | b.Property("PackagesId") 139 | .HasColumnType("TEXT"); 140 | 141 | b.Property("ResourcesId") 142 | .HasColumnType("TEXT"); 143 | 144 | b.HasKey("PackagesId", "ResourcesId"); 145 | 146 | b.HasIndex("ResourcesId"); 147 | 148 | b.ToTable("PackageResource"); 149 | }); 150 | 151 | modelBuilder.Entity("PackageResource", b => 152 | { 153 | b.HasOne("MaaDownloadServer.Model.Entities.Package", null) 154 | .WithMany() 155 | .HasForeignKey("PackagesId") 156 | .OnDelete(DeleteBehavior.Cascade) 157 | .IsRequired(); 158 | 159 | b.HasOne("MaaDownloadServer.Model.Entities.Resource", null) 160 | .WithMany() 161 | .HasForeignKey("ResourcesId") 162 | .OnDelete(DeleteBehavior.Cascade) 163 | .IsRequired(); 164 | }); 165 | #pragma warning restore 612, 618 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /MaaDownloadServer/Migrations/20220316053837_RemoveGameData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | #nullable disable 5 | 6 | namespace MaaDownloadServer.Migrations 7 | { 8 | public partial class RemoveGameData : Migration 9 | { 10 | protected override void Up(MigrationBuilder migrationBuilder) 11 | { 12 | migrationBuilder.DropTable( 13 | name: "ark_penguin_item"); 14 | 15 | migrationBuilder.DropTable( 16 | name: "ark_penguin_stage"); 17 | 18 | migrationBuilder.DropTable( 19 | name: "ark_prts_item"); 20 | 21 | migrationBuilder.DropTable( 22 | name: "ark_penguin_zone"); 23 | } 24 | 25 | protected override void Down(MigrationBuilder migrationBuilder) 26 | { 27 | migrationBuilder.CreateTable( 28 | name: "ark_penguin_item", 29 | columns: table => new 30 | { 31 | item_id = table.Column(type: "TEXT", nullable: false), 32 | cn_exist = table.Column(type: "INTEGER", nullable: false), 33 | en_name = table.Column(type: "TEXT", nullable: true), 34 | item_type = table.Column(type: "TEXT", nullable: true), 35 | ja_name = table.Column(type: "TEXT", nullable: true), 36 | jp_exist = table.Column(type: "INTEGER", nullable: false), 37 | ko_name = table.Column(type: "TEXT", nullable: true), 38 | kr_exist = table.Column(type: "INTEGER", nullable: false), 39 | name = table.Column(type: "TEXT", nullable: true), 40 | rarity = table.Column(type: "INTEGER", nullable: false), 41 | sort_id = table.Column(type: "INTEGER", nullable: false), 42 | us_exist = table.Column(type: "INTEGER", nullable: false), 43 | zh_name = table.Column(type: "TEXT", nullable: true) 44 | }, 45 | constraints: table => 46 | { 47 | table.PrimaryKey("PK_ark_penguin_item", x => x.item_id); 48 | }); 49 | 50 | migrationBuilder.CreateTable( 51 | name: "ark_penguin_zone", 52 | columns: table => new 53 | { 54 | zone_id = table.Column(type: "TEXT", nullable: false), 55 | background = table.Column(type: "TEXT", nullable: true), 56 | background_file_name = table.Column(type: "TEXT", nullable: true), 57 | cn_exist = table.Column(type: "INTEGER", nullable: false), 58 | en_zone_name = table.Column(type: "TEXT", nullable: true), 59 | ja_zone_name = table.Column(type: "TEXT", nullable: true), 60 | jp_exist = table.Column(type: "INTEGER", nullable: false), 61 | ko_zone_name = table.Column(type: "TEXT", nullable: true), 62 | kr_exist = table.Column(type: "INTEGER", nullable: false), 63 | us_exist = table.Column(type: "INTEGER", nullable: false), 64 | zh_zone_name = table.Column(type: "TEXT", nullable: true), 65 | zone_name = table.Column(type: "TEXT", nullable: true), 66 | zone_type = table.Column(type: "TEXT", nullable: true) 67 | }, 68 | constraints: table => 69 | { 70 | table.PrimaryKey("PK_ark_penguin_zone", x => x.zone_id); 71 | }); 72 | 73 | migrationBuilder.CreateTable( 74 | name: "ark_prts_item", 75 | columns: table => new 76 | { 77 | id = table.Column(type: "TEXT", nullable: false), 78 | category = table.Column(type: "TEXT", nullable: true), 79 | description = table.Column(type: "TEXT", nullable: true), 80 | image = table.Column(type: "TEXT", nullable: true), 81 | image_download_url = table.Column(type: "TEXT", nullable: true), 82 | item_id = table.Column(type: "TEXT", nullable: true), 83 | name = table.Column(type: "TEXT", nullable: true), 84 | obtain = table.Column(type: "TEXT", nullable: true), 85 | rarity = table.Column(type: "INTEGER", nullable: false), 86 | usage = table.Column(type: "TEXT", nullable: true) 87 | }, 88 | constraints: table => 89 | { 90 | table.PrimaryKey("PK_ark_prts_item", x => x.id); 91 | }); 92 | 93 | migrationBuilder.CreateTable( 94 | name: "ark_penguin_stage", 95 | columns: table => new 96 | { 97 | stage_id = table.Column(type: "TEXT", nullable: false), 98 | ArkPenguinZoneZoneId = table.Column(type: "TEXT", nullable: true), 99 | cn_close_time = table.Column(type: "TEXT", nullable: true), 100 | cn_exist = table.Column(type: "INTEGER", nullable: false), 101 | cn_open_time = table.Column(type: "TEXT", nullable: true), 102 | drop_items = table.Column(type: "TEXT", nullable: true), 103 | en_stage_code = table.Column(type: "TEXT", nullable: true), 104 | ja_stage_code = table.Column(type: "TEXT", nullable: true), 105 | jp_close_time = table.Column(type: "TEXT", nullable: true), 106 | jp_exist = table.Column(type: "INTEGER", nullable: false), 107 | jp_open_time = table.Column(type: "TEXT", nullable: true), 108 | ko_stage_code = table.Column(type: "TEXT", nullable: true), 109 | kr_close_time = table.Column(type: "TEXT", nullable: true), 110 | kr_exist = table.Column(type: "INTEGER", nullable: false), 111 | kr_open_time = table.Column(type: "TEXT", nullable: true), 112 | min_clear_time = table.Column(type: "INTEGER", nullable: false), 113 | stage_ap_cost = table.Column(type: "INTEGER", nullable: false), 114 | stage_code = table.Column(type: "TEXT", nullable: true), 115 | stage_type = table.Column(type: "TEXT", nullable: true), 116 | us_close_time = table.Column(type: "TEXT", nullable: true), 117 | us_exist = table.Column(type: "INTEGER", nullable: false), 118 | us_open_time = table.Column(type: "TEXT", nullable: true), 119 | zh_stage_code = table.Column(type: "TEXT", nullable: true) 120 | }, 121 | constraints: table => 122 | { 123 | table.PrimaryKey("PK_ark_penguin_stage", x => x.stage_id); 124 | table.ForeignKey( 125 | name: "FK_ark_penguin_stage_ark_penguin_zone_ArkPenguinZoneZoneId", 126 | column: x => x.ArkPenguinZoneZoneId, 127 | principalTable: "ark_penguin_zone", 128 | principalColumn: "zone_id"); 129 | }); 130 | 131 | migrationBuilder.CreateIndex( 132 | name: "IX_ark_penguin_stage_ArkPenguinZoneZoneId", 133 | table: "ark_penguin_stage", 134 | column: "ArkPenguinZoneZoneId"); 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /MaaDownloadServer/Migrations/20220318190006_AddDownloadCount.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using MaaDownloadServer.Database; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Migrations; 7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 8 | 9 | #nullable disable 10 | 11 | namespace MaaDownloadServer.Migrations 12 | { 13 | [DbContext(typeof(MaaDownloadServerDbContext))] 14 | [Migration("20220318190006_AddDownloadCount")] 15 | partial class AddDownloadCount 16 | { 17 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 18 | { 19 | #pragma warning disable 612, 618 20 | modelBuilder.HasAnnotation("ProductVersion", "6.0.3"); 21 | 22 | modelBuilder.Entity("MaaDownloadServer.Model.Entities.DatabaseCache", b => 23 | { 24 | b.Property("Id") 25 | .ValueGeneratedOnAdd() 26 | .HasColumnType("TEXT") 27 | .HasColumnName("id"); 28 | 29 | b.Property("QueryId") 30 | .HasColumnType("TEXT") 31 | .HasColumnName("query_id"); 32 | 33 | b.Property("Value") 34 | .HasColumnType("TEXT") 35 | .HasColumnName("value"); 36 | 37 | b.HasKey("Id"); 38 | 39 | b.ToTable("database_cache"); 40 | }); 41 | 42 | modelBuilder.Entity("MaaDownloadServer.Model.Entities.DownloadCount", b => 43 | { 44 | b.Property("Id") 45 | .ValueGeneratedOnAdd() 46 | .HasColumnType("TEXT") 47 | .HasColumnName("id"); 48 | 49 | b.Property("ComponentName") 50 | .HasColumnType("TEXT") 51 | .HasColumnName("component_name"); 52 | 53 | b.Property("Count") 54 | .HasColumnType("INTEGER") 55 | .HasColumnName("count"); 56 | 57 | b.Property("FromVersion") 58 | .HasColumnType("TEXT") 59 | .HasColumnName("from_version"); 60 | 61 | b.Property("ToVersion") 62 | .HasColumnType("TEXT") 63 | .HasColumnName("to_version"); 64 | 65 | b.HasKey("Id"); 66 | 67 | b.ToTable("download_count"); 68 | }); 69 | 70 | modelBuilder.Entity("MaaDownloadServer.Model.Entities.Package", b => 71 | { 72 | b.Property("Id") 73 | .ValueGeneratedOnAdd() 74 | .HasColumnType("TEXT") 75 | .HasColumnName("id"); 76 | 77 | b.Property("Architecture") 78 | .IsRequired() 79 | .HasColumnType("TEXT") 80 | .HasColumnName("architecture"); 81 | 82 | b.Property("Component") 83 | .HasColumnType("TEXT") 84 | .HasColumnName("component"); 85 | 86 | b.Property("Platform") 87 | .IsRequired() 88 | .HasColumnType("TEXT") 89 | .HasColumnName("platform"); 90 | 91 | b.Property("PublishTime") 92 | .HasColumnType("TEXT") 93 | .HasColumnName("publish_time"); 94 | 95 | b.Property("UpdateLog") 96 | .HasColumnType("TEXT") 97 | .HasColumnName("update_log"); 98 | 99 | b.Property("Version") 100 | .HasColumnType("TEXT") 101 | .HasColumnName("version"); 102 | 103 | b.HasKey("Id"); 104 | 105 | b.ToTable("package"); 106 | }); 107 | 108 | modelBuilder.Entity("MaaDownloadServer.Model.Entities.PublicContent", b => 109 | { 110 | b.Property("Id") 111 | .ValueGeneratedOnAdd() 112 | .HasColumnType("TEXT") 113 | .HasColumnName("id"); 114 | 115 | b.Property("AddTime") 116 | .HasColumnType("TEXT") 117 | .HasColumnName("add_time"); 118 | 119 | b.Property("Duration") 120 | .HasColumnType("TEXT") 121 | .HasColumnName("duration"); 122 | 123 | b.Property("FileExtension") 124 | .HasColumnType("TEXT") 125 | .HasColumnName("file_extension"); 126 | 127 | b.Property("Hash") 128 | .HasColumnType("TEXT") 129 | .HasColumnName("hash"); 130 | 131 | b.Property("Tag") 132 | .HasColumnType("TEXT") 133 | .HasColumnName("tag"); 134 | 135 | b.HasKey("Id"); 136 | 137 | b.ToTable("public_content"); 138 | }); 139 | 140 | modelBuilder.Entity("MaaDownloadServer.Model.Entities.Resource", b => 141 | { 142 | b.Property("Id") 143 | .ValueGeneratedOnAdd() 144 | .HasColumnType("TEXT") 145 | .HasColumnName("id"); 146 | 147 | b.Property("FileName") 148 | .HasColumnType("TEXT") 149 | .HasColumnName("file_name"); 150 | 151 | b.Property("Hash") 152 | .HasColumnType("TEXT") 153 | .HasColumnName("hash"); 154 | 155 | b.Property("Path") 156 | .HasColumnType("TEXT") 157 | .HasColumnName("path"); 158 | 159 | b.HasKey("Id"); 160 | 161 | b.ToTable("resource"); 162 | }); 163 | 164 | modelBuilder.Entity("PackageResource", b => 165 | { 166 | b.Property("PackagesId") 167 | .HasColumnType("TEXT"); 168 | 169 | b.Property("ResourcesId") 170 | .HasColumnType("TEXT"); 171 | 172 | b.HasKey("PackagesId", "ResourcesId"); 173 | 174 | b.HasIndex("ResourcesId"); 175 | 176 | b.ToTable("PackageResource"); 177 | }); 178 | 179 | modelBuilder.Entity("PackageResource", b => 180 | { 181 | b.HasOne("MaaDownloadServer.Model.Entities.Package", null) 182 | .WithMany() 183 | .HasForeignKey("PackagesId") 184 | .OnDelete(DeleteBehavior.Cascade) 185 | .IsRequired(); 186 | 187 | b.HasOne("MaaDownloadServer.Model.Entities.Resource", null) 188 | .WithMany() 189 | .HasForeignKey("ResourcesId") 190 | .OnDelete(DeleteBehavior.Cascade) 191 | .IsRequired(); 192 | }); 193 | #pragma warning restore 612, 618 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /MaaDownloadServer/Migrations/20220318190006_AddDownloadCount.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | #nullable disable 5 | 6 | namespace MaaDownloadServer.Migrations 7 | { 8 | public partial class AddDownloadCount : Migration 9 | { 10 | protected override void Up(MigrationBuilder migrationBuilder) 11 | { 12 | migrationBuilder.CreateTable( 13 | name: "download_count", 14 | columns: table => new 15 | { 16 | id = table.Column(type: "TEXT", nullable: false), 17 | component_name = table.Column(type: "TEXT", nullable: true), 18 | from_version = table.Column(type: "TEXT", nullable: true), 19 | to_version = table.Column(type: "TEXT", nullable: true), 20 | count = table.Column(type: "INTEGER", nullable: false) 21 | }, 22 | constraints: table => 23 | { 24 | table.PrimaryKey("PK_download_count", x => x.id); 25 | }); 26 | } 27 | 28 | protected override void Down(MigrationBuilder migrationBuilder) 29 | { 30 | migrationBuilder.DropTable( 31 | name: "download_count"); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /MaaDownloadServer/Migrations/MaaDownloadServerDbContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using MaaDownloadServer.Database; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 7 | 8 | #nullable disable 9 | 10 | namespace MaaDownloadServer.Migrations 11 | { 12 | [DbContext(typeof(MaaDownloadServerDbContext))] 13 | partial class MaaDownloadServerDbContextModelSnapshot : ModelSnapshot 14 | { 15 | protected override void BuildModel(ModelBuilder modelBuilder) 16 | { 17 | #pragma warning disable 612, 618 18 | modelBuilder.HasAnnotation("ProductVersion", "6.0.3"); 19 | 20 | modelBuilder.Entity("MaaDownloadServer.Model.Entities.DatabaseCache", b => 21 | { 22 | b.Property("Id") 23 | .ValueGeneratedOnAdd() 24 | .HasColumnType("TEXT") 25 | .HasColumnName("id"); 26 | 27 | b.Property("QueryId") 28 | .HasColumnType("TEXT") 29 | .HasColumnName("query_id"); 30 | 31 | b.Property("Value") 32 | .HasColumnType("TEXT") 33 | .HasColumnName("value"); 34 | 35 | b.HasKey("Id"); 36 | 37 | b.ToTable("database_cache"); 38 | }); 39 | 40 | modelBuilder.Entity("MaaDownloadServer.Model.Entities.DownloadCount", b => 41 | { 42 | b.Property("Id") 43 | .ValueGeneratedOnAdd() 44 | .HasColumnType("TEXT") 45 | .HasColumnName("id"); 46 | 47 | b.Property("ComponentName") 48 | .HasColumnType("TEXT") 49 | .HasColumnName("component_name"); 50 | 51 | b.Property("Count") 52 | .HasColumnType("INTEGER") 53 | .HasColumnName("count"); 54 | 55 | b.Property("FromVersion") 56 | .HasColumnType("TEXT") 57 | .HasColumnName("from_version"); 58 | 59 | b.Property("ToVersion") 60 | .HasColumnType("TEXT") 61 | .HasColumnName("to_version"); 62 | 63 | b.HasKey("Id"); 64 | 65 | b.ToTable("download_count"); 66 | }); 67 | 68 | modelBuilder.Entity("MaaDownloadServer.Model.Entities.Package", b => 69 | { 70 | b.Property("Id") 71 | .ValueGeneratedOnAdd() 72 | .HasColumnType("TEXT") 73 | .HasColumnName("id"); 74 | 75 | b.Property("Architecture") 76 | .IsRequired() 77 | .HasColumnType("TEXT") 78 | .HasColumnName("architecture"); 79 | 80 | b.Property("Component") 81 | .HasColumnType("TEXT") 82 | .HasColumnName("component"); 83 | 84 | b.Property("Platform") 85 | .IsRequired() 86 | .HasColumnType("TEXT") 87 | .HasColumnName("platform"); 88 | 89 | b.Property("PublishTime") 90 | .HasColumnType("TEXT") 91 | .HasColumnName("publish_time"); 92 | 93 | b.Property("UpdateLog") 94 | .HasColumnType("TEXT") 95 | .HasColumnName("update_log"); 96 | 97 | b.Property("Version") 98 | .HasColumnType("TEXT") 99 | .HasColumnName("version"); 100 | 101 | b.HasKey("Id"); 102 | 103 | b.ToTable("package"); 104 | }); 105 | 106 | modelBuilder.Entity("MaaDownloadServer.Model.Entities.PublicContent", b => 107 | { 108 | b.Property("Id") 109 | .ValueGeneratedOnAdd() 110 | .HasColumnType("TEXT") 111 | .HasColumnName("id"); 112 | 113 | b.Property("AddTime") 114 | .HasColumnType("TEXT") 115 | .HasColumnName("add_time"); 116 | 117 | b.Property("Duration") 118 | .HasColumnType("TEXT") 119 | .HasColumnName("duration"); 120 | 121 | b.Property("FileExtension") 122 | .HasColumnType("TEXT") 123 | .HasColumnName("file_extension"); 124 | 125 | b.Property("Hash") 126 | .HasColumnType("TEXT") 127 | .HasColumnName("hash"); 128 | 129 | b.Property("Tag") 130 | .HasColumnType("TEXT") 131 | .HasColumnName("tag"); 132 | 133 | b.HasKey("Id"); 134 | 135 | b.ToTable("public_content"); 136 | }); 137 | 138 | modelBuilder.Entity("MaaDownloadServer.Model.Entities.Resource", b => 139 | { 140 | b.Property("Id") 141 | .ValueGeneratedOnAdd() 142 | .HasColumnType("TEXT") 143 | .HasColumnName("id"); 144 | 145 | b.Property("FileName") 146 | .HasColumnType("TEXT") 147 | .HasColumnName("file_name"); 148 | 149 | b.Property("Hash") 150 | .HasColumnType("TEXT") 151 | .HasColumnName("hash"); 152 | 153 | b.Property("Path") 154 | .HasColumnType("TEXT") 155 | .HasColumnName("path"); 156 | 157 | b.HasKey("Id"); 158 | 159 | b.ToTable("resource"); 160 | }); 161 | 162 | modelBuilder.Entity("PackageResource", b => 163 | { 164 | b.Property("PackagesId") 165 | .HasColumnType("TEXT"); 166 | 167 | b.Property("ResourcesId") 168 | .HasColumnType("TEXT"); 169 | 170 | b.HasKey("PackagesId", "ResourcesId"); 171 | 172 | b.HasIndex("ResourcesId"); 173 | 174 | b.ToTable("PackageResource"); 175 | }); 176 | 177 | modelBuilder.Entity("PackageResource", b => 178 | { 179 | b.HasOne("MaaDownloadServer.Model.Entities.Package", null) 180 | .WithMany() 181 | .HasForeignKey("PackagesId") 182 | .OnDelete(DeleteBehavior.Cascade) 183 | .IsRequired(); 184 | 185 | b.HasOne("MaaDownloadServer.Model.Entities.Resource", null) 186 | .WithMany() 187 | .HasForeignKey("ResourcesId") 188 | .OnDelete(DeleteBehavior.Cascade) 189 | .IsRequired(); 190 | }); 191 | #pragma warning restore 612, 618 192 | } 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /MaaDownloadServer/Model/Attributes/ConfigurationSectionAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace MaaDownloadServer.Model.Attributes; 2 | 3 | [AttributeUsage(AttributeTargets.Class)] 4 | public class ConfigurationSectionAttribute : MaaAttribute 5 | { 6 | private readonly string _sectionName; 7 | 8 | public ConfigurationSectionAttribute(string sectionName) 9 | { 10 | _sectionName = sectionName; 11 | } 12 | 13 | public override string GetValue() 14 | { 15 | return _sectionName; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /MaaDownloadServer/Model/Attributes/MaaAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace MaaDownloadServer.Model.Attributes; 2 | 3 | public abstract class MaaAttribute : Attribute 4 | { 5 | public abstract string GetValue(); 6 | } 7 | -------------------------------------------------------------------------------- /MaaDownloadServer/Model/Dto/Announce/Announce.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace MaaDownloadServer.Model.Dto; 4 | 5 | public record Announce( 6 | [property: JsonPropertyName("id")] Guid Id, 7 | [property: JsonPropertyName("time")] DateTime Time, 8 | [property: JsonPropertyName("level"), JsonConverter(typeof(JsonStringEnumConverter))] AnnounceLevel Level, 9 | [property: JsonPropertyName("issuer")] string Issuer, 10 | [property: JsonPropertyName("message")] string Message); 11 | -------------------------------------------------------------------------------- /MaaDownloadServer/Model/Dto/ComponentController/ComponentDto.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace MaaDownloadServer.Model.Dto; 4 | 5 | public record ComponentDto( 6 | [property: JsonPropertyName("name")] string Name, 7 | [property: JsonPropertyName("description")] string Description); 8 | -------------------------------------------------------------------------------- /MaaDownloadServer/Model/Dto/ComponentController/GetComponentDetailDto.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace MaaDownloadServer.Model.Dto; 4 | 5 | public record GetComponentDetailDto( 6 | [property: JsonPropertyName("name")] string Name, 7 | [property: JsonPropertyName("description")] string Description, 8 | [property: JsonPropertyName("versions")] List Versions, 9 | [property: JsonPropertyName("page")] int Page, 10 | [property: JsonPropertyName("limit")] int Limit); 11 | -------------------------------------------------------------------------------- /MaaDownloadServer/Model/Dto/DownloadController/GetDownloadUrlDto.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace MaaDownloadServer.Model.Dto; 4 | 5 | public record GetDownloadUrlDto( 6 | [property: JsonPropertyName("platform")] string Platform, 7 | [property: JsonPropertyName("arch")] string Architecture, 8 | [property: JsonPropertyName("version")] string Version, 9 | [property: JsonPropertyName("url")] string Url, 10 | [property: JsonPropertyName("hash")] string Hash); 11 | -------------------------------------------------------------------------------- /MaaDownloadServer/Model/Dto/General/ComponentSupport.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace MaaDownloadServer.Model.Dto; 4 | 5 | public record ComponentSupport( 6 | [property: JsonPropertyName("platform")] string Platform, 7 | [property: JsonPropertyName("arch")] string Arch); 8 | -------------------------------------------------------------------------------- /MaaDownloadServer/Model/Dto/General/ComponentVersions.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace MaaDownloadServer.Model.Dto; 4 | 5 | public record ComponentVersions( 6 | [property: JsonPropertyName("version")] string Version, 7 | [property: JsonPropertyName("publish_time")] DateTime PublishTime, 8 | [property: JsonPropertyName("update_log")] string UpdateLog, 9 | [property: JsonPropertyName("support")] List Supports); 10 | -------------------------------------------------------------------------------- /MaaDownloadServer/Model/Dto/General/ResourceMetadata.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace MaaDownloadServer.Model.Dto; 4 | 5 | public record ResourceMetadata( 6 | [property: JsonPropertyName("file_name")] string FileName, 7 | [property: JsonPropertyName("path")] string Path, 8 | [property: JsonPropertyName("hash")] string Hash); 9 | -------------------------------------------------------------------------------- /MaaDownloadServer/Model/Dto/General/VersionDetail.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace MaaDownloadServer.Model.Dto; 4 | 5 | public record VersionDetail( 6 | [property: JsonPropertyName("version")] string Version, 7 | [property: JsonPropertyName("publish_time")] DateTime PublishTime, 8 | [property: JsonPropertyName("update_log")] string UpdateLog, 9 | [property: JsonPropertyName("resources")] List Resources); 10 | -------------------------------------------------------------------------------- /MaaDownloadServer/Model/Dto/VersionController/GetVersionDto.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace MaaDownloadServer.Model.Dto; 4 | 5 | public record GetVersionDto( 6 | [property: JsonPropertyName("platform")] string Platform, 7 | [property: JsonPropertyName("arch")] string Arch, 8 | [property: JsonPropertyName("details")] VersionDetail VersionDetail); 9 | -------------------------------------------------------------------------------- /MaaDownloadServer/Model/Entities/DatabaseCache.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations.Schema; 2 | 3 | namespace MaaDownloadServer.Model.Entities; 4 | 5 | /// 6 | /// 临时数据表 7 | /// 8 | [Table("database_cache")] 9 | public record DatabaseCache 10 | { 11 | /// 12 | /// ID 13 | /// 14 | [Column("id")] 15 | public Guid Id { get; set; } 16 | 17 | /// 18 | /// 检索 ID 19 | /// 20 | [Column("query_id")] 21 | public string QueryId { get; set; } 22 | 23 | /// 24 | /// 值 25 | /// 26 | [Column("value")] 27 | public string Value { get; set; } 28 | } 29 | -------------------------------------------------------------------------------- /MaaDownloadServer/Model/Entities/DownloadCount.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations.Schema; 2 | 3 | namespace MaaDownloadServer.Model.Entities; 4 | 5 | /// 6 | /// 下载文件 API 访问次数统计 7 | /// 8 | [Table("download_count")] 9 | public record DownloadCount 10 | { 11 | /// 12 | /// ID 13 | /// 14 | [Column("id")] 15 | public Guid Id { get; set; } 16 | 17 | /// 18 | /// 组件名 19 | /// 20 | [Column("component_name")] 21 | public string ComponentName { get; set; } 22 | 23 | /// 24 | /// 源版本 25 | /// 26 | [Column("from_version")] 27 | public string FromVersion { get; set; } 28 | 29 | /// 30 | /// 目标版本,为空表示版本完整包 31 | /// 32 | [Column("to_version")] 33 | public string ToVersion { get; set; } 34 | 35 | /// 36 | /// 总计下载次数 37 | /// 38 | [Column("count")] 39 | public int Count { get; set; } 40 | }; 41 | -------------------------------------------------------------------------------- /MaaDownloadServer/Model/Entities/Package.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations.Schema; 2 | 3 | namespace MaaDownloadServer.Model.Entities; 4 | 5 | /// 6 | /// 包(一个平台的所有文件) 7 | /// 8 | /// ID 9 | /// 组件名称 10 | /// 版本 11 | /// 平台 12 | /// 架构 13 | /// 发布时间 14 | /// 更新日志 15 | [Table("package")] 16 | public record Package(Guid Id, string Component, string Version, 17 | Platform Platform, Architecture Architecture, 18 | DateTime PublishTime, string UpdateLog) 19 | { 20 | /// 21 | /// 包 Id 22 | /// 23 | [Column("id")] 24 | public Guid Id { get; set; } = Id; 25 | 26 | /// 27 | /// 版本 28 | /// 29 | [Column("version")] 30 | public string Version { get; set; } = Version; 31 | 32 | /// 33 | /// 平台 34 | /// 35 | [Column("platform")] 36 | public Platform Platform { get; set; } = Platform; 37 | 38 | /// 39 | /// 架构 40 | /// 41 | [Column("architecture")] 42 | public Architecture Architecture { get; set; } = Architecture; 43 | 44 | /// 45 | /// 资源 46 | /// 47 | [Column("resources")] 48 | public List Resources { get; set; } = new(); 49 | 50 | /// 51 | /// 发包时间 52 | /// 53 | [Column("publish_time")] 54 | public DateTime PublishTime { get; set; } = PublishTime; 55 | 56 | /// 57 | /// 更新日志 58 | /// 59 | [Column("update_log")] 60 | public string UpdateLog { get; set; } = UpdateLog; 61 | 62 | /// 63 | /// 组件名称 64 | /// 65 | [Column("component")] 66 | public string Component { get; set; } = Component; 67 | } 68 | -------------------------------------------------------------------------------- /MaaDownloadServer/Model/Entities/PublicContent.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations.Schema; 2 | 3 | namespace MaaDownloadServer.Model.Entities; 4 | 5 | /// 6 | /// 可下载的包 7 | /// 8 | /// ID 9 | /// 文件后缀 10 | /// 标签 11 | /// 添加时间 12 | /// MD5 校验码 13 | /// 过期时间 14 | [Table("public_content")] 15 | public record PublicContent(Guid Id, string FileExtension, string Tag, DateTime AddTime, string Hash, DateTime Duration) 16 | { 17 | /// 18 | /// 公共资源 ID 19 | /// 20 | [Column("id")] 21 | public Guid Id { get; set; } = Id; 22 | 23 | /// 24 | /// 文件扩展名,不含点号 25 | /// 26 | [Column("file_extension")] 27 | public string FileExtension { get; set; } = FileExtension; 28 | 29 | /// 30 | /// 标签 31 | /// 32 | [Column("tag")] 33 | public string Tag { get; set; } = Tag; 34 | 35 | /// 36 | /// 过期时间 37 | /// 38 | [Column("duration")] 39 | public DateTime Duration { get; set; } = Duration; 40 | 41 | /// 42 | /// 文件 MD5 校验 43 | /// 44 | [Column("hash")] 45 | public string Hash { get; set; } = Hash; 46 | 47 | /// 48 | /// 添加时间 49 | /// 50 | [Column("add_time")] 51 | public DateTime AddTime { get; set; } = AddTime; 52 | } 53 | -------------------------------------------------------------------------------- /MaaDownloadServer/Model/Entities/Resource.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations.Schema; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace MaaDownloadServer.Model.Entities; 5 | 6 | /// 7 | /// 资源(单指一个文件) 8 | /// 9 | /// ID 10 | /// 文件名 11 | /// 保存路径 12 | /// MD5 校验码 13 | [Table("resource")] 14 | public record Resource(Guid Id, string FileName, string Path, string Hash) 15 | { 16 | /// 17 | /// 资源 Id 18 | /// 19 | [Column("id")] 20 | [JsonPropertyName("id")] 21 | public Guid Id { get; set; } = Id; 22 | 23 | /// 24 | /// 文件名 25 | /// 26 | [Column("file_name")] 27 | [JsonPropertyName("file_name")] 28 | public string FileName { get; set; } = FileName; 29 | 30 | /// 31 | /// 文件保存路径 32 | /// 33 | [Column("path")] 34 | [JsonPropertyName("path")] 35 | public string Path { get; set; } = Path; 36 | 37 | /// 38 | /// 文件 MD5 哈希值 39 | /// 40 | [Column("hash")] 41 | [JsonPropertyName("hash")] 42 | public string Hash { get; set; } = Hash; 43 | 44 | /// 45 | /// 对应包 46 | /// 47 | [Column("packages")] 48 | [JsonIgnore] 49 | public List Packages { get; set; } 50 | } 51 | -------------------------------------------------------------------------------- /MaaDownloadServer/Model/External/Script/AfterDownloadProcessOperation.cs: -------------------------------------------------------------------------------- 1 | namespace MaaDownloadServer.Model.External; 2 | 3 | public enum AfterDownloadProcessOperation 4 | { 5 | Unzip, 6 | None, 7 | Custom 8 | } 9 | -------------------------------------------------------------------------------- /MaaDownloadServer/Model/External/Script/BeforeAddProcessOperation.cs: -------------------------------------------------------------------------------- 1 | namespace MaaDownloadServer.Model.External; 2 | 3 | public enum BeforeAddProcessOperation 4 | { 5 | Zip, 6 | None, 7 | Custom 8 | } 9 | -------------------------------------------------------------------------------- /MaaDownloadServer/Model/External/Script/ComponentConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace MaaDownloadServer.Model.External; 4 | 5 | public class ComponentConfiguration 6 | { 7 | [JsonPropertyName("name")] 8 | public string Name { get; set; } 9 | [JsonPropertyName("description")] 10 | public string Description { get; set; } 11 | 12 | [JsonPropertyName("metadata_urls")] 13 | public List MetadataUrl { get; set; } 14 | 15 | [JsonPropertyName("default_url_placeholder")] 16 | public Dictionary UrlPlaceholder { get; set; } 17 | 18 | [JsonPropertyName("after_download_process")] 19 | public PreProcess AfterDownloadProcess { get; set; } 20 | 21 | [JsonPropertyName("before_add_process")] 22 | public PreProcess BeforeAddProcess { get; set; } 23 | 24 | [JsonPropertyName("scripts")] 25 | public Scripts Scripts { get; set; } 26 | 27 | [JsonPropertyName("use_proxy")] 28 | public bool UseProxy { get; set; } 29 | 30 | [JsonPropertyName("pack_update_package")] 31 | public bool PackUpdatePackage { get; set; } 32 | 33 | [JsonPropertyName("interval")] 34 | public int Interval { get; set; } 35 | } 36 | -------------------------------------------------------------------------------- /MaaDownloadServer/Model/External/Script/PreProcess.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace MaaDownloadServer.Model.External; 5 | 6 | public class PreProcess where T : Enum 7 | { 8 | [JsonPropertyName("operation")] 9 | [JsonConverter(typeof(JsonStringEnumConverter))] 10 | public T Operation { get; set; } 11 | 12 | [JsonPropertyName("args")] 13 | public JsonElement Args { get; set; } 14 | } 15 | -------------------------------------------------------------------------------- /MaaDownloadServer/Model/External/Script/Scripts.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace MaaDownloadServer.Model.External; 4 | 5 | public class Scripts 6 | { 7 | [JsonPropertyName("get_download_info")] 8 | public string GetDownloadInfo { get; set; } 9 | 10 | [JsonPropertyName("after_download_process")] 11 | public string AfterDownloadProcess { get; set; } 12 | 13 | [JsonPropertyName("before_add_process")] 14 | public string BeforeAddProcess { get; set; } 15 | 16 | [JsonPropertyName("relative_path_calculation")] 17 | public string RelativePathCalculation { get; set; } 18 | } 19 | -------------------------------------------------------------------------------- /MaaDownloadServer/Model/General/DownloadContentInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace MaaDownloadServer.Model.General; 4 | 5 | public record DownloadContentInfo 6 | { 7 | [JsonIgnore] 8 | public Guid Id { get; } = Guid.NewGuid(); 9 | 10 | [JsonPropertyName("version")] 11 | public string Version { get; init; } 12 | 13 | [JsonPropertyName("download_url")] 14 | public string DownloadUrl { get; init; } 15 | 16 | [JsonPropertyName("platform")] 17 | [JsonConverter(typeof(JsonStringEnumConverter))] 18 | public Platform Platform { get; init; } 19 | 20 | [JsonPropertyName("arch")] 21 | [JsonConverter(typeof(JsonStringEnumConverter))] 22 | public Architecture Architecture { get; init; } 23 | 24 | [JsonPropertyName("file_extension")] 25 | public string FileExtension { get; init; } 26 | 27 | [JsonPropertyName("checksum")] 28 | public string Checksum { get; init; } 29 | 30 | [JsonPropertyName("checksum_type")] 31 | [JsonConverter(typeof(JsonStringEnumConverter))] 32 | public ChecksumType ChecksumType { get; init; } 33 | 34 | [JsonPropertyName("update_time")] 35 | public DateTime UpdateTime { get; init; } 36 | 37 | [JsonPropertyName("update_log")] 38 | public string UpdateLog { get; init; } 39 | } 40 | -------------------------------------------------------------------------------- /MaaDownloadServer/Model/General/PublicContentTag.cs: -------------------------------------------------------------------------------- 1 | using Semver; 2 | 3 | namespace MaaDownloadServer.Model.General; 4 | 5 | public record PublicContentTag( 6 | PublicContentTagType Type, 7 | Platform Platform, 8 | Architecture Architecture, 9 | string Component, 10 | SemVersion Version, 11 | SemVersion Target = null); 12 | -------------------------------------------------------------------------------- /MaaDownloadServer/Model/General/ResourceInfo.cs: -------------------------------------------------------------------------------- 1 | namespace MaaDownloadServer.Model.General; 2 | 3 | public record ResourceInfo(string Path, string RelativePath, string Hash) 4 | { 5 | public string Path { get; set; } = Path; 6 | public string RelativePath { get; set; } = RelativePath; 7 | public string Hash { get; set; } = Hash; 8 | } 9 | -------------------------------------------------------------------------------- /MaaDownloadServer/Model/General/UpdateDiff.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace MaaDownloadServer.Model.General; 4 | 5 | public record UpdateDiff(string StartVersion, string TargetVersion, Platform Platform, Architecture Architecture, List NewResources, List UnNeededResources) 6 | { 7 | [JsonPropertyName("start_version")] 8 | public string StartVersion { get; set; } = StartVersion; 9 | 10 | [JsonPropertyName("target_version")] 11 | public string TargetVersion { get; set; } = TargetVersion; 12 | 13 | [JsonConverter(typeof(JsonStringEnumConverter))] 14 | [JsonPropertyName("platform")] 15 | public Platform Platform { get; set; } = Platform; 16 | 17 | [JsonPropertyName("architecture")] 18 | [JsonConverter(typeof(JsonStringEnumConverter))] 19 | public Architecture Architecture { get; set; } = Architecture; 20 | 21 | [JsonPropertyName("new_resources")] 22 | public List NewResources { get; set; } = NewResources; 23 | 24 | [JsonPropertyName("unneeded_resources")] 25 | public List UnNeededResources { get; set; } = UnNeededResources; 26 | } 27 | -------------------------------------------------------------------------------- /MaaDownloadServer/Model/Options/AnnounceOption.cs: -------------------------------------------------------------------------------- 1 | namespace MaaDownloadServer.Model.Options; 2 | 3 | [ConfigurationSection("MaaServer:Announce")] 4 | public record AnnounceOption : IMaaOption 5 | { 6 | public string[] ServerChanSendKeys { get; set; } 7 | } 8 | -------------------------------------------------------------------------------- /MaaDownloadServer/Model/Options/DataDirectoriesOption.cs: -------------------------------------------------------------------------------- 1 | namespace MaaDownloadServer.Model.Options; 2 | 3 | [ConfigurationSection("MaaServer:DataDirectories")] 4 | public record DataDirectoriesOption : IMaaOption 5 | { 6 | public string RootPath { get; set; } 7 | public DataDirectoriesSubDirectoriesOption SubDirectories { get; set; } 8 | 9 | public string Downloads => Path.Combine(RootPath, SubDirectories.Downloads); 10 | public string Public => Path.Combine(RootPath, SubDirectories.Public); 11 | public string Resources => Path.Combine(RootPath, SubDirectories.Resources); 12 | public string Database => Path.Combine(RootPath, SubDirectories.Database); 13 | public string Temp => Path.Combine(RootPath, SubDirectories.Temp); 14 | public string Scripts => Path.Combine(RootPath, SubDirectories.Scripts); 15 | public string Static => Path.Combine(RootPath, SubDirectories.Static); 16 | public string VirtualEnvironments => Path.Combine(RootPath, SubDirectories.VirtualEnvironments); 17 | } 18 | -------------------------------------------------------------------------------- /MaaDownloadServer/Model/Options/IMaaOption.cs: -------------------------------------------------------------------------------- 1 | namespace MaaDownloadServer.Model.Options; 2 | 3 | public interface IMaaOption { } 4 | -------------------------------------------------------------------------------- /MaaDownloadServer/Model/Options/NetworkOption.cs: -------------------------------------------------------------------------------- 1 | namespace MaaDownloadServer.Model.Options; 2 | 3 | [ConfigurationSection("MaaServer:Network")] 4 | public record NetworkOption : IMaaOption 5 | { 6 | public string Proxy { get; set; } 7 | public string UserAgent { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /MaaDownloadServer/Model/Options/PublicContentOption.cs: -------------------------------------------------------------------------------- 1 | namespace MaaDownloadServer.Model.Options; 2 | 3 | [ConfigurationSection("MaaServer:PublicContent")] 4 | public record PublicContentOption : IMaaOption 5 | { 6 | public int OutdatedCheckInterval { get; set; } 7 | public int DefaultDuration { get; set; } 8 | public int AutoBundledDuration { get; set; } 9 | } 10 | -------------------------------------------------------------------------------- /MaaDownloadServer/Model/Options/ScriptEngineOption.cs: -------------------------------------------------------------------------------- 1 | namespace MaaDownloadServer.Model.Options; 2 | 3 | [ConfigurationSection("MaaServer:ScriptEngine")] 4 | public record ScriptEngineOption : IMaaOption 5 | { 6 | public string Python { get; set; } 7 | } 8 | -------------------------------------------------------------------------------- /MaaDownloadServer/Model/Options/ServerOption.cs: -------------------------------------------------------------------------------- 1 | namespace MaaDownloadServer.Model.Options; 2 | 3 | [ConfigurationSection("MaaServer:Server")] 4 | public record ServerOption : IMaaOption 5 | { 6 | public string Host { get; set; } 7 | public int Port { get; set; } 8 | public string ApiFullUrl { get; set; } 9 | } 10 | -------------------------------------------------------------------------------- /MaaDownloadServer/Model/Options/SubOptions/DataDirectoriesSubDirectoriesOption.cs: -------------------------------------------------------------------------------- 1 | namespace MaaDownloadServer.Model.Options; 2 | 3 | public record DataDirectoriesSubDirectoriesOption 4 | { 5 | public string Downloads { get; set; } 6 | public string Public { get; set; } 7 | public string Resources { get; set; } 8 | public string Database { get; set; } 9 | public string Temp { get; set; } 10 | public string Scripts { get; set; } 11 | public string Static { get; set; } 12 | public string VirtualEnvironments { get; set; } 13 | } 14 | -------------------------------------------------------------------------------- /MaaDownloadServer/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using System.Text.Json; 3 | using System.Web; 4 | using AspNetCoreRateLimit; 5 | using MaaDownloadServer.External; 6 | using MaaDownloadServer.Jobs; 7 | using MaaDownloadServer.Middleware; 8 | using Microsoft.EntityFrameworkCore; 9 | using Microsoft.Extensions.FileProviders; 10 | using Microsoft.Net.Http.Headers; 11 | using Quartz; 12 | using Serilog; 13 | using Serilog.Extensions.Logging; 14 | 15 | var maaConfigurationProvider = MaaConfigurationProvider.GetProvider(); 16 | if (maaConfigurationProvider is null) 17 | { 18 | Environment.Exit(ProgramExitCode.ConfigurationProviderIsNull); 19 | } 20 | 21 | #region Build configuration and logger 22 | 23 | Log.Logger = new LoggerConfiguration() 24 | .ReadFrom.Configuration(maaConfigurationProvider.GetConfiguration()) 25 | .CreateLogger(); 26 | 27 | Log.Logger.Information("启动中..."); 28 | Log.Logger.Information("程序集版本:{AssemblyVersion}", 29 | maaConfigurationProvider.GetConfiguration().GetValue("AssemblyVersion")); 30 | 31 | #endregion 32 | 33 | #region Data directories 34 | 35 | var dataDirectoriesOption = maaConfigurationProvider.GetOption().Value; 36 | 37 | if (MaaConfigurationProvider.IsNoDataDirectoryCheck() is false) 38 | { 39 | if (Directory.Exists(dataDirectoriesOption.RootPath) is false) 40 | { 41 | Directory.CreateDirectory(dataDirectoriesOption.RootPath); 42 | } 43 | 44 | var directoryCheck = (string path) => 45 | { 46 | if (Directory.Exists(path) is false) 47 | { 48 | Directory.CreateDirectory(path!); 49 | } 50 | }; 51 | 52 | directoryCheck.Invoke(dataDirectoriesOption.Downloads); 53 | directoryCheck.Invoke(dataDirectoriesOption.Public); 54 | directoryCheck.Invoke(dataDirectoriesOption.Resources); 55 | directoryCheck.Invoke(dataDirectoriesOption.Database); 56 | directoryCheck.Invoke(dataDirectoriesOption.Temp); 57 | directoryCheck.Invoke(dataDirectoriesOption.Scripts); 58 | directoryCheck.Invoke(dataDirectoriesOption.Static); 59 | directoryCheck.Invoke(dataDirectoriesOption.VirtualEnvironments); 60 | } 61 | else 62 | { 63 | Log.Logger.Warning("跳过了数据目录检查"); 64 | } 65 | 66 | #endregion 67 | 68 | #region Python environment and script configuration 69 | 70 | if (MaaConfigurationProvider.IsNoPythonCheck()) 71 | { 72 | Log.Logger.Warning("跳过了 Python 环境检查"); 73 | } 74 | 75 | var scriptEngineOption = maaConfigurationProvider.GetOption().Value; 76 | 77 | var logger = new SerilogLoggerFactory(Log.Logger).CreateLogger(); 78 | 79 | if (MaaConfigurationProvider.IsNoPythonCheck()) 80 | { 81 | // Check Python Interpreter Exist 82 | var pythonInterpreterExist = Python.EnvironmentCheck(logger, scriptEngineOption.Python); 83 | if (pythonInterpreterExist is false) 84 | { 85 | Log.Logger.Fatal("Python 解释器不存在,请检查配置"); 86 | Environment.Exit(ProgramExitCode.NoPythonInterpreter); 87 | } 88 | } 89 | 90 | // Init Python environment 91 | var scriptDirectories = new DirectoryInfo(dataDirectoriesOption.Scripts).GetDirectories(); 92 | 93 | var componentConfigurations = new List(); 94 | foreach (var scriptDirectory in scriptDirectories) 95 | { 96 | var configurationFile = Path.Combine(scriptDirectory.FullName, "component.json"); 97 | if (File.Exists(configurationFile) is false) 98 | { 99 | Environment.Exit(ProgramExitCode.ScriptDoNotHaveConfigFile); 100 | } 101 | 102 | try 103 | { 104 | await using var configFileStream = File.OpenRead(configurationFile); 105 | var configObj = JsonSerializer.Deserialize(configFileStream); 106 | componentConfigurations.Add(configObj); 107 | } 108 | catch (Exception ex) 109 | { 110 | logger.LogCritical(ex, "解析组件配置文件失败"); 111 | Environment.Exit(ProgramExitCode.FailedToParseScriptConfigFile); 112 | } 113 | 114 | if (MaaConfigurationProvider.IsNoPythonCheck() is false) 115 | { 116 | var venvDirectory = Path.Combine( 117 | dataDirectoriesOption.VirtualEnvironments, 118 | scriptDirectory.Name); 119 | var requirements = scriptDirectory.GetFiles().FirstOrDefault(x => x.Name == "requirements.txt"); 120 | var pyVenvCreateStatus = Python.CreateVirtualEnvironment(logger, scriptEngineOption.Python, venvDirectory, requirements?.FullName); 121 | if (pyVenvCreateStatus is false) 122 | { 123 | logger.LogCritical("Python 虚拟环境创建失败,venvDirectory: {VenvDirectory}", venvDirectory); 124 | Environment.Exit(ProgramExitCode.FailedToCreatePythonVenv); 125 | } 126 | } 127 | } 128 | 129 | #endregion 130 | 131 | var builder = WebApplication.CreateBuilder(args); 132 | 133 | if (MaaConfigurationProvider.IsInsideDocker()) 134 | { 135 | var serverOption = maaConfigurationProvider.GetOption().Value; 136 | var url = $"http://{serverOption.Host}:{serverOption.Port}"; 137 | builder.WebHost.UseUrls(url); 138 | } 139 | else 140 | { 141 | Log.Logger.Information("在 Docker Container 中运行,忽略 MaaServer:Server:Host 和 MaaServer:Server:Port 配置项"); 142 | } 143 | 144 | #region Web application builder 145 | 146 | builder.Host.UseSerilog(); 147 | builder.Configuration.AddConfiguration(MaaConfigurationProvider.GetProvider().GetConfiguration()); 148 | 149 | builder.Services.AddMaaOptions(MaaConfigurationProvider.GetProvider()); 150 | 151 | builder.Services.AddMaaDownloadServerDbContext(); 152 | builder.Services.AddControllers(); 153 | builder.Services.AddMaaServices(); 154 | builder.Services.AddHttpClients(MaaConfigurationProvider.GetProvider()); 155 | builder.Services.AddMemoryCache(); 156 | builder.Services.AddResponseCaching(); 157 | 158 | builder.Services.AddInMemoryRateLimiting(); 159 | builder.Services.AddSingleton(); 160 | 161 | builder.Services.AddQuartzJobs(maaConfigurationProvider.GetOption(), componentConfigurations); 162 | builder.Services.AddQuartzServer(options => 163 | { 164 | options.WaitForJobsToComplete = true; 165 | }); 166 | builder.Services.AddCors(options => 167 | { 168 | options.AddDefaultPolicy(policy => 169 | { 170 | policy.AllowAnyOrigin() 171 | .AllowAnyHeader() 172 | .AllowAnyMethod(); 173 | }); 174 | }); 175 | 176 | #endregion 177 | 178 | var app = builder.Build(); 179 | 180 | #region Database check 181 | 182 | using var scope = app.Services.CreateScope(); 183 | await using var dbContext = scope.ServiceProvider.GetService(); 184 | 185 | if (File.Exists(Path.Combine(dataDirectoriesOption.Database, "data.db")) is false) 186 | { 187 | Log.Logger.Information("数据库文件不存在,准备创建新的数据库文件"); 188 | dbContext!.Database.Migrate(); 189 | Log.Logger.Information("数据库创建完成"); 190 | } 191 | 192 | var dbCaches = await dbContext!.DatabaseCaches 193 | .Where(x => x.QueryId.StartsWith("persist_") == false) 194 | .ToListAsync(); 195 | dbContext!.DatabaseCaches.RemoveRange(dbCaches); 196 | 197 | #endregion 198 | 199 | #region Add component name and description 200 | 201 | var componentInfosDbCache = componentConfigurations 202 | .Select(x => new ComponentDto(x.Name, x.Description)) 203 | .Select(x => JsonSerializer.Serialize(x)) 204 | .Select(x => new DatabaseCache { Id = Guid.NewGuid(), QueryId = "Component", Value = x }) 205 | .ToList(); 206 | 207 | await dbContext!.DatabaseCaches.AddRangeAsync(componentInfosDbCache); 208 | await dbContext!.SaveChangesAsync(); 209 | 210 | Log.Logger.Information("已添加 {C} 个 Component", componentInfosDbCache.Count); 211 | 212 | await dbContext!.DisposeAsync(); 213 | 214 | #endregion 215 | 216 | app.UseSerilogRequestLogging(config => 217 | { 218 | config.IncludeQueryInRequestPath = true; 219 | }); 220 | 221 | app.UseIpRateLimiting(); 222 | 223 | app.UseCors(); 224 | 225 | app.UseMiddleware(); 226 | 227 | #region File server middleware 228 | 229 | app.UseFileServer(new FileServerOptions 230 | { 231 | StaticFileOptions = 232 | { 233 | DefaultContentType = "application/octet-stream", 234 | OnPrepareResponse = context => 235 | { 236 | var fn = context.File.Name; 237 | 238 | if (fn is null) 239 | { 240 | return; 241 | } 242 | 243 | var encodedName = HttpUtility.UrlEncode(fn, Encoding.UTF8); 244 | context.Context.Response.Headers.Add("content-disposition", $"attachment; filename={encodedName}"); 245 | } 246 | }, 247 | FileProvider = new PhysicalFileProvider(dataDirectoriesOption.Public), 248 | RequestPath = "/files", 249 | EnableDirectoryBrowsing = false, 250 | EnableDefaultFiles = false, 251 | RedirectToAppendTrailingSlash = false, 252 | }); 253 | 254 | app.UseFileServer(new FileServerOptions 255 | { 256 | StaticFileOptions = 257 | { 258 | DefaultContentType = "application/octet-stream", 259 | OnPrepareResponse = context => 260 | { 261 | var fn = context.File.Name; 262 | 263 | if (fn is null) 264 | { 265 | return; 266 | } 267 | 268 | var encodedName = HttpUtility.UrlEncode(fn, Encoding.UTF8); 269 | context.Context.Response.Headers.Add("content-disposition", $"attachment; filename={encodedName}"); 270 | } 271 | }, 272 | FileProvider = new PhysicalFileProvider(dataDirectoriesOption.Static), 273 | RequestPath = "/static", 274 | EnableDirectoryBrowsing = false, 275 | EnableDefaultFiles = false, 276 | RedirectToAppendTrailingSlash = false, 277 | }); 278 | 279 | #endregion 280 | 281 | #region Response Caching 282 | 283 | app.UseResponseCaching(); 284 | app.Use(async (context, next) => 285 | { 286 | context.Response.GetTypedHeaders().CacheControl = 287 | new CacheControlHeaderValue { Public = true, MaxAge = TimeSpan.FromMinutes(5) }; 288 | context.Response.Headers[HeaderNames.Vary] = 289 | new[] { "Accept-Encoding" }; 290 | 291 | await next(context); 292 | }); 293 | 294 | #endregion 295 | 296 | app.MapControllers(); 297 | 298 | app.Run(); 299 | -------------------------------------------------------------------------------- /MaaDownloadServer/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:33137", 7 | "sslPort": 44345 8 | } 9 | }, 10 | "profiles": { 11 | "MaaServer.Download": { 12 | "commandName": "Project", 13 | "dotnetRunMessages": true, 14 | "launchBrowser": false, 15 | "applicationUrl": "https://localhost:7102;http://localhost:5089", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "IIS Express": { 21 | "commandName": "IISExpress", 22 | "launchBrowser": false, 23 | "environmentVariables": { 24 | "ASPNETCORE_ENVIRONMENT": "Development" 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /MaaDownloadServer/Providers/MaaConfigurationProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Microsoft.Extensions.Options; 3 | 4 | namespace MaaDownloadServer.Providers; 5 | 6 | public class MaaConfigurationProvider 7 | { 8 | private static MaaConfigurationProvider s_provider; 9 | private readonly IConfiguration _configuration; 10 | 11 | private MaaConfigurationProvider(string assemblyPath, string dataDirectory) 12 | { 13 | var configFile = Path.Combine(dataDirectory, "appsettings.json"); 14 | 15 | var configurationBuilder = new ConfigurationBuilder() 16 | .AddJsonFile(configFile, false, true); 17 | 18 | if (IsDevelopment()) 19 | { 20 | configurationBuilder.AddJsonFile(Path.Combine(dataDirectory, "appsettings.Development.json"), true, true); 21 | } 22 | 23 | var azureAppConfigurationConnectionString = Environment.GetEnvironmentVariable("MAADS_AZURE_APP_CONFIGURATION"); 24 | if (string.IsNullOrEmpty(azureAppConfigurationConnectionString) is false) 25 | { 26 | configurationBuilder.AddAzureAppConfiguration(azureAppConfigurationConnectionString); 27 | } 28 | 29 | configurationBuilder.AddEnvironmentVariables("MAADS_"); 30 | configurationBuilder.AddCommandLine(Environment.GetCommandLineArgs()); 31 | 32 | var version = Assembly.GetExecutingAssembly().GetName().Version; 33 | var versionString = "0.0.0"; 34 | if (version is not null) 35 | { 36 | versionString = $"{version.Major}.{version.Minor}.{version.Revision}"; 37 | } 38 | configurationBuilder.AddInMemoryCollection(new List> 39 | { 40 | new("AssemblyPath", assemblyPath), 41 | new("ConfigurationFile", configFile), 42 | new("DataDirectory", dataDirectory), 43 | new("AssemblyVersion", versionString) 44 | }); 45 | 46 | if (IsDevelopment()) 47 | { 48 | configurationBuilder.AddInMemoryCollection(new List> 49 | { 50 | new("DevConfigurationFile", Path.Combine(dataDirectory, "appsettings.Development.json")), 51 | }); 52 | } 53 | 54 | _configuration = configurationBuilder.Build(); 55 | } 56 | 57 | public static MaaConfigurationProvider GetProvider() 58 | { 59 | if (s_provider is not null) 60 | { 61 | return s_provider; 62 | } 63 | 64 | CreateProvider(); 65 | return s_provider; 66 | } 67 | 68 | private static void CreateProvider() 69 | { 70 | var dataDirectoryEnvironmentVariable = Environment.GetEnvironmentVariable("MAADS_DATA_DIRECTORY"); 71 | var assemblyPath = new FileInfo(Assembly.GetExecutingAssembly().Location).Directory!.FullName; 72 | 73 | var dataDirectory = string.IsNullOrEmpty(dataDirectoryEnvironmentVariable) 74 | ? new DirectoryInfo(Path.Combine(assemblyPath, "data")) 75 | : new DirectoryInfo(dataDirectoryEnvironmentVariable); 76 | 77 | if (dataDirectory.Exists is false) 78 | { 79 | dataDirectory.Create(); 80 | } 81 | 82 | var configurationFileExist = dataDirectory.GetFiles("appsettings.json").Length == 1; 83 | 84 | if (configurationFileExist is false) 85 | { 86 | var appSettingString = File.ReadAllTextAsync(Path.Combine(assemblyPath, "appsettings.json")).Result; 87 | appSettingString = appSettingString.Replace("{{DATA DIRECTORY}}", dataDirectory.FullName); 88 | File.WriteAllTextAsync(Path.Combine(dataDirectory.FullName, "appsettings.json"), appSettingString).Wait(); 89 | Console.WriteLine($"配置文件不存在, 已复制新的 appsettings.json 至 {dataDirectory.FullName} 路径, 请修改配置文件"); 90 | Environment.Exit(0); 91 | } 92 | 93 | if (IsDevelopment()) 94 | { 95 | if (File.Exists(Path.Combine(assemblyPath, "appsettings.Development.json")) && 96 | File.Exists(Path.Combine(dataDirectory.FullName, "appsettings.Development.json")) is false) 97 | { 98 | File.Copy(Path.Combine(assemblyPath, "appsettings.Development.json"), 99 | Path.Combine(dataDirectory.FullName, "appsettings.Development.json")); 100 | } 101 | } 102 | 103 | s_provider = new MaaConfigurationProvider(assemblyPath, dataDirectory.FullName); 104 | } 105 | 106 | public static bool IsInsideDocker() 107 | { 108 | return Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") is "true"; 109 | } 110 | 111 | public static bool IsDevelopment() 112 | { 113 | return GetFromEnvAndArgs("ASPNETCORE_ENVIRONMENT", "Development"); 114 | } 115 | 116 | public static bool IsNoDataDirectoryCheck() 117 | { 118 | return GetFromEnvAndArgs("NO_DATA_DIRECTORY_CHECK"); 119 | } 120 | 121 | public static bool IsNoPythonCheck() 122 | { 123 | return GetFromEnvAndArgs("NO_PYTHON_CHECK"); 124 | } 125 | 126 | private static bool GetFromEnvAndArgs(string name, string value = null) 127 | { 128 | if (value is not null) 129 | { 130 | if (Environment.GetEnvironmentVariable(name) == value) 131 | { 132 | return true; 133 | } 134 | } 135 | else 136 | { 137 | if (Environment.GetEnvironmentVariable(name)?.ToLower() == "true") 138 | { 139 | return true; 140 | } 141 | } 142 | 143 | var inArgs = Environment.GetCommandLineArgs().FirstOrDefault(x => x.StartsWith(name)); 144 | if (inArgs is null) 145 | { 146 | return false; 147 | } 148 | 149 | var status = inArgs.Replace($"{name}=", ""); 150 | return status == value; 151 | } 152 | 153 | public IConfiguration GetConfiguration() 154 | { 155 | return _configuration; 156 | } 157 | 158 | public IOptions GetOption() where T : class, IMaaOption, new() 159 | { 160 | var obj = new T(); 161 | var sectionName = AttributeUtil.ReadAttributeValue(); 162 | _configuration.Bind(sectionName, obj); 163 | var option = Options.Create(obj); 164 | return option; 165 | } 166 | 167 | public IConfigurationSection GetConfigurationSection(string key) 168 | { 169 | return _configuration.GetSection(key); 170 | } 171 | 172 | public IConfigurationSection GetOptionConfigurationSection() where T : class, IMaaOption, new() 173 | { 174 | var sectionName = AttributeUtil.ReadAttributeValue(); 175 | return _configuration.GetSection(sectionName); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /MaaDownloadServer/Services/Base/AnnounceService.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.Extensions.Options; 4 | 5 | namespace MaaDownloadServer.Services.Base; 6 | 7 | public class AnnounceService : IAnnounceService 8 | { 9 | private readonly MaaDownloadServerDbContext _dbContext; 10 | private readonly IHttpClientFactory _httpClientFactory; 11 | private readonly IOptions _announceOption; 12 | private readonly ILogger _logger; 13 | 14 | public AnnounceService(MaaDownloadServerDbContext dbContext, 15 | IHttpClientFactory httpClientFactory, 16 | IOptions announceOption, 17 | ILogger logger) 18 | { 19 | _dbContext = dbContext; 20 | _httpClientFactory = httpClientFactory; 21 | _announceOption = announceOption; 22 | _logger = logger; 23 | } 24 | 25 | public async Task AddAnnounce(string issuer, string title, string message, AnnounceLevel level = AnnounceLevel.Information) 26 | { 27 | _logger.LogInformation("加入新的 Announce: [{Level}] {Issuer}: {Message}", level, issuer, message); 28 | 29 | #region Database 30 | 31 | var existAnnounce = 32 | await _dbContext.DatabaseCaches.FirstOrDefaultAsync(x => x.QueryId == $"persist_anno_{issuer}"); 33 | if (existAnnounce is not null) 34 | { 35 | _dbContext.Remove(existAnnounce); 36 | } 37 | 38 | var plainApiMessageString = $"{title} - {message}"; 39 | var id = Guid.NewGuid(); 40 | var newAnnounce = new Announce(id, DateTime.Now, level, issuer, plainApiMessageString); 41 | var newAnnounceString = JsonSerializer.Serialize(newAnnounce); 42 | _dbContext.DatabaseCaches.Add(new DatabaseCache 43 | { 44 | Id = id, 45 | QueryId = $"persist_anno_{issuer}", 46 | Value = newAnnounceString 47 | }); 48 | 49 | await _dbContext.SaveChangesAsync(); 50 | 51 | #endregion 52 | 53 | #region Server Chan 54 | 55 | if (_announceOption.Value.ServerChanSendKeys.Length == 0) 56 | { 57 | return; 58 | } 59 | 60 | var levelShort = level switch 61 | { 62 | AnnounceLevel.Information => "INF", 63 | AnnounceLevel.Warning => "WRN", 64 | AnnounceLevel.Error => "ERR", 65 | _ => throw new ArgumentOutOfRangeException(nameof(level), level, null) 66 | }; 67 | var serverChanMessageContent = $"[{levelShort}] {message}"; 68 | var form = new List> 69 | { 70 | new("title", title), 71 | new("desp", serverChanMessageContent) 72 | }; 73 | var serverChanHttpClient = _httpClientFactory.CreateClient("ServerChan"); 74 | 75 | foreach (var key in _announceOption.Value.ServerChanSendKeys) 76 | { 77 | var responseMessage = await serverChanHttpClient.PostAsync($"{key}.send", new FormUrlEncodedContent(form)); 78 | var body = await responseMessage.Content.ReadAsStringAsync(); 79 | if (responseMessage.IsSuccessStatusCode is false) 80 | { 81 | _logger.LogError("推送消息至 ServerChan 失败,状态码:{ServerChanStatusCode},消息体:{ServerChanContent}", 82 | responseMessage.StatusCode, body); 83 | } 84 | } 85 | 86 | #endregion 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /MaaDownloadServer/Services/Base/ConfigurationService.cs: -------------------------------------------------------------------------------- 1 | namespace MaaDownloadServer.Services.Base; 2 | 3 | public class ConfigurationService : IConfigurationService 4 | { 5 | private readonly IConfiguration _configuration; 6 | 7 | public ConfigurationService(IConfiguration configuration) 8 | { 9 | _configuration = configuration; 10 | } 11 | 12 | public string GetPublicDirectory() 13 | { 14 | return Path.Combine( 15 | _configuration["MaaServer:DataDirectories:RootPath"], 16 | _configuration["MaaServer:DataDirectories:SubDirectories:Public"]); 17 | } 18 | 19 | public string GetDownloadDirectory() 20 | { 21 | return Path.Combine( 22 | _configuration["MaaServer:DataDirectories:RootPath"], 23 | _configuration["MaaServer:DataDirectories:SubDirectories:Downloads"]); 24 | } 25 | 26 | public string GetResourcesDirectory() 27 | { 28 | return Path.Combine( 29 | _configuration["MaaServer:DataDirectories:RootPath"], 30 | _configuration["MaaServer:DataDirectories:SubDirectories:Resources"]); 31 | } 32 | 33 | public string GetTempDirectory() 34 | { 35 | return Path.Combine( 36 | _configuration["MaaServer:DataDirectories:RootPath"], 37 | _configuration["MaaServer:DataDirectories:SubDirectories:Temp"]); 38 | } 39 | 40 | public int GetPublicContentDefaultDuration() 41 | { 42 | return _configuration.GetValue("MaaServer:PublicContent:DefaultDuration"); 43 | } 44 | 45 | public int GetPublicContentAutoBundledDuration() 46 | { 47 | return _configuration.GetValue("MaaServer:PublicContent:AutoBundledDuration"); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /MaaDownloadServer/Services/Base/FileSystemService.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.IO.Compression; 3 | using System.Text.Json; 4 | 5 | namespace MaaDownloadServer.Services.Base; 6 | 7 | public class FileSystemService : IFileSystemService 8 | { 9 | private readonly MaaDownloadServerDbContext _dbContext; 10 | private readonly ILogger _logger; 11 | private readonly IConfigurationService _configurationService; 12 | 13 | public FileSystemService( 14 | MaaDownloadServerDbContext dbContext, 15 | ILogger logger, 16 | IConfigurationService configurationService) 17 | { 18 | _dbContext = dbContext; 19 | _logger = logger; 20 | _configurationService = configurationService; 21 | } 22 | 23 | /// 24 | public string CreateZipFile(string sourceFolder, string targetName, CompressionLevel level = CompressionLevel.NoCompression, bool deleteSource = false) 25 | { 26 | if (targetName is null) 27 | { 28 | throw new ArgumentNullException(nameof(targetName)); 29 | } 30 | 31 | if (Directory.Exists(sourceFolder) is false) 32 | { 33 | throw new DirectoryNotFoundException($"文件夹 {sourceFolder} 不存在"); 34 | } 35 | 36 | if (File.Exists(targetName)) 37 | { 38 | throw new FileNotFoundException($"文件 {targetName} 已存在"); 39 | } 40 | 41 | ZipFile.CreateFromDirectory(sourceFolder, targetName, level, false); 42 | if (deleteSource) 43 | { 44 | Directory.Delete(sourceFolder, true); 45 | } 46 | 47 | return targetName; 48 | } 49 | 50 | /// 51 | public string CreateZipFile(IEnumerable sourceFiles, IEnumerable sourceDirectories, 52 | string targetName, CompressionLevel level = CompressionLevel.NoCompression, bool deleteSource = false) 53 | { 54 | var randomId = Guid.NewGuid().ToString(); 55 | var tempFolder = Path.Combine(_configurationService.GetTempDirectory(), randomId); 56 | Directory.CreateDirectory(tempFolder); 57 | var fileEnumerable = sourceFiles as string[] ?? sourceFiles.ToArray(); 58 | var directoryEnumerable = sourceDirectories as string[] ?? sourceDirectories.ToArray(); 59 | foreach (var sourceFile in fileEnumerable) 60 | { 61 | var fi = new FileInfo(sourceFile); 62 | if (fi.Exists) 63 | { 64 | fi.CopyTo(Path.Combine(tempFolder, fi.Name)); 65 | continue; 66 | } 67 | Directory.Delete(tempFolder, true); 68 | throw new FileNotFoundException($"文件 {sourceFile} 不存在"); 69 | } 70 | 71 | foreach (var sourceDirectory in directoryEnumerable) 72 | { 73 | var di = new DirectoryInfo(sourceDirectory); 74 | if (di.Exists) 75 | { 76 | di.CopyTo(Path.Combine(tempFolder, di.Name)); 77 | continue; 78 | } 79 | Directory.Delete(tempFolder, true); 80 | throw new DirectoryNotFoundException($"文件夹 {sourceDirectory} 不存在"); 81 | } 82 | 83 | var result = CreateZipFile(tempFolder, targetName, level, deleteSource); 84 | 85 | if (deleteSource is false) 86 | { 87 | return result; 88 | } 89 | 90 | foreach (var sourceFile in fileEnumerable) 91 | { 92 | if (File.Exists(sourceFile)) 93 | { 94 | File.Delete(sourceFile); 95 | } 96 | } 97 | 98 | foreach (var sourceDirectory in directoryEnumerable) 99 | { 100 | if (Directory.Exists(sourceDirectory)) 101 | { 102 | Directory.Delete(sourceDirectory, true); 103 | } 104 | } 105 | 106 | return result; 107 | } 108 | 109 | /// 110 | public async Task AddFullPackage(Guid jobId, string componentName, DownloadContentInfo downloadContentInfo) 111 | { 112 | var path = Path.Combine( 113 | _configurationService.GetDownloadDirectory(), 114 | jobId.ToString(), 115 | $"{downloadContentInfo.Id}.{downloadContentInfo.FileExtension}"); 116 | if (File.Exists(path) is false) 117 | { 118 | _logger.LogError("正在准备复制完整包至 Public 但是文件 {Path} 不存在", path); 119 | return null; 120 | } 121 | 122 | var hash = HashUtil.ComputeFileMd5Hash(path); 123 | var publicContentTag = new PublicContentTag(PublicContentTagType.FullPackage, downloadContentInfo.Platform, 124 | downloadContentInfo.Architecture, componentName, downloadContentInfo.Version.ParseToSemVer()); 125 | 126 | var pc = new PublicContent( 127 | downloadContentInfo.Id, 128 | downloadContentInfo.FileExtension, 129 | publicContentTag.ParseToTagString(), 130 | DateTime.Now, 131 | hash, 132 | DateTime.Now.AddDays(_configurationService.GetPublicContentAutoBundledDuration())); 133 | var targetPath = Path.Combine( 134 | _configurationService.GetPublicDirectory(), 135 | $"{downloadContentInfo.Id}.{downloadContentInfo.FileExtension}"); 136 | File.Copy(path, targetPath); 137 | await _dbContext.PublicContents.AddAsync(pc); 138 | await _dbContext.SaveChangesAsync(); 139 | return pc; 140 | } 141 | 142 | /// 143 | public async Task AddNewResources(List res) 144 | { 145 | var resources = new List(); 146 | foreach (var (path, relativePath, hash) in res) 147 | { 148 | var id = Guid.NewGuid(); 149 | var name = Path.GetFileName(path); 150 | _logger.LogDebug("添加新的资源文件 [{Id}] {Path} ({Hash})", id, name, hash); 151 | resources.Add(new Resource(id, name, relativePath, hash)); 152 | Debug.Assert(path != null, "r.Path != null"); 153 | File.Move(path, Path.Combine( 154 | _configurationService.GetResourcesDirectory(), hash)); 155 | } 156 | await _dbContext.Resources.AddRangeAsync(resources); 157 | await _dbContext.SaveChangesAsync(); 158 | } 159 | 160 | /// 161 | public void CleanDownloadDirectory(Guid jobId) 162 | { 163 | var di = new DirectoryInfo(Path.Combine(_configurationService.GetDownloadDirectory(), jobId.ToString())); 164 | if (di.Exists is false) 165 | { 166 | return; 167 | } 168 | 169 | _logger.LogInformation("正在清理下载目录 Job = {JobId}", jobId); 170 | di.Delete(true); 171 | } 172 | 173 | /// 174 | public UpdateDiff GetUpdateDiff(Package fromPackage, Package toPackage) 175 | { 176 | // 在新版本中选择 路径/文件名/Hash 三者存在不同的文件,为新增文件 177 | // 可能是二进制文件更新导致 Hash 不同,可能是文件移动导致路径不同,可能是文件重命名导致文件名不同 178 | var newRes = (from r in toPackage.Resources 179 | let isOld = fromPackage.Resources.Exists(x => x.Hash == r.Hash && x.FileName == r.FileName && x.Path == r.Path) 180 | where isOld is false 181 | select r).ToList(); 182 | // 不需要的指 旧版本存在,但是新版本中,同路径、同文件名、同 Hash 的文件不存在的资源 183 | var unNeededRes = (from r in fromPackage.Resources 184 | let isOld = toPackage.Resources.Exists(x => x.Hash == r.Hash && x.FileName == r.FileName && x.Path == r.Path) 185 | where isOld is false 186 | select r).ToList(); 187 | var diff = new UpdateDiff(fromPackage.Version, toPackage.Version, 188 | toPackage.Platform, toPackage.Architecture, 189 | newRes, unNeededRes); 190 | return diff; 191 | } 192 | 193 | /// 194 | public async Task> AddUpdatePackages(string componentName, List diffs) 195 | { 196 | var pcs = new List(); 197 | foreach (var diff in diffs) 198 | { 199 | var id = Guid.NewGuid(); 200 | _logger.LogInformation("打包更新包 {StartVer} -> {TargetVer},ID = {Id}", 201 | diff.StartVersion, diff.TargetVersion, id); 202 | var tempFolder = new DirectoryInfo(Path.Combine(_configurationService.GetTempDirectory(), id.ToString())); 203 | tempFolder.Create(); 204 | var pcTag = new PublicContentTag(PublicContentTagType.UpdatePackage, diff.Platform, diff.Architecture, 205 | componentName, diff.StartVersion.ParseToSemVer(), diff.TargetVersion.ParseToSemVer()).ParseToTagString(); 206 | foreach (var newResource in diff.NewResources) 207 | { 208 | File.Copy(Path.Combine(_configurationService.GetResourcesDirectory(), newResource.Hash), 209 | Path.Combine(tempFolder.FullName, newResource.FileName)); 210 | _logger.LogDebug("打包更新包 {Id},复制新资源 {ResName} ({Hash})", 211 | id, newResource.FileName, newResource.Hash); 212 | } 213 | var updatePackageLog = JsonSerializer.Serialize(diff); 214 | await File.WriteAllTextAsync(Path.Combine(tempFolder.FullName, "update_log.json"), updatePackageLog); 215 | var zipFile = Path.Combine(_configurationService.GetTempDirectory(), $"{id}.zip"); 216 | ZipFile.CreateFromDirectory(tempFolder.FullName, zipFile); 217 | var hash = HashUtil.ComputeFileMd5Hash(zipFile); 218 | pcs.Add(new PublicContent(id, "zip", pcTag, DateTime.Now, hash, DateTime.Now.AddDays(_configurationService.GetPublicContentDefaultDuration()))); 219 | File.Move(zipFile, Path.Combine(_configurationService.GetPublicDirectory(), $"{id}.zip")); 220 | tempFolder.Delete(true); 221 | _logger.LogInformation("已打包更新包 {Id},MD5校验 = {Hash}", id, hash); 222 | } 223 | await _dbContext.PublicContents.AddRangeAsync(pcs); 224 | await _dbContext.SaveChangesAsync(); 225 | return pcs; 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /MaaDownloadServer/Services/Base/Interfaces/IAnnounceService.cs: -------------------------------------------------------------------------------- 1 | namespace MaaDownloadServer.Services.Base.Interfaces; 2 | 3 | public interface IAnnounceService 4 | { 5 | Task AddAnnounce(string issuer, string title, string message, AnnounceLevel level = AnnounceLevel.Information); 6 | } 7 | -------------------------------------------------------------------------------- /MaaDownloadServer/Services/Base/Interfaces/IConfigurationService.cs: -------------------------------------------------------------------------------- 1 | namespace MaaDownloadServer.Services.Base.Interfaces; 2 | 3 | public interface IConfigurationService 4 | { 5 | string GetPublicDirectory(); 6 | string GetDownloadDirectory(); 7 | string GetResourcesDirectory(); 8 | string GetTempDirectory(); 9 | int GetPublicContentDefaultDuration(); 10 | int GetPublicContentAutoBundledDuration(); 11 | } 12 | -------------------------------------------------------------------------------- /MaaDownloadServer/Services/Base/Interfaces/IFileSystemService.cs: -------------------------------------------------------------------------------- 1 | using System.IO.Compression; 2 | 3 | namespace MaaDownloadServer.Services.Base.Interfaces; 4 | 5 | public interface IFileSystemService 6 | { 7 | /// 8 | /// 创建压缩包 9 | /// 10 | /// 源文件夹 11 | /// 目标文件位置,扩展名必须为 .zip 12 | /// 压缩等级 13 | /// 是否删除源 14 | /// 创建的压缩包文件路径 15 | string CreateZipFile(string sourceFolder, string targetName, CompressionLevel level = CompressionLevel.NoCompression, bool deleteSource = false); 16 | 17 | /// 18 | /// 创建压缩包 19 | /// 20 | /// 源文件 21 | /// 源文件夹 22 | /// 目标文件位置,扩展名必须为 .zip 23 | /// 压缩等级 24 | /// 是否删除源 25 | /// 26 | string CreateZipFile(IEnumerable sourceFiles, IEnumerable sourceDirectories, string targetName, CompressionLevel level = CompressionLevel.NoCompression, bool deleteSource = false); 27 | 28 | /// 29 | /// 添加完整包至 Public 30 | /// 31 | /// 本次 Job 的 Id,用于寻找完整包位置 32 | /// 组件名 33 | /// 下载元数据 34 | /// PublicContent 实体 35 | Task AddFullPackage(Guid jobId, string componentName, DownloadContentInfo downloadContentInfo); 36 | 37 | /// 38 | /// 添加新的资源文件 39 | /// 40 | /// 资源文件信息表 41 | /// 42 | Task AddNewResources(List res); 43 | 44 | /// 45 | /// 清空下载目录 46 | /// 47 | /// 本次 Job 的 Id 48 | void CleanDownloadDirectory(Guid jobId); 49 | 50 | /// 51 | /// 获取更新 Diff 52 | /// 53 | /// 起始版本 54 | /// 目标版本 55 | /// 56 | UpdateDiff GetUpdateDiff(Package fromPackage, Package toPackage); 57 | 58 | /// 59 | /// 创建更新包 60 | /// 61 | /// 组件名 62 | /// 更新 Diff 列表 63 | /// PublicContent 实体 List 64 | public Task> AddUpdatePackages(string componentName, List diffs); 65 | } 66 | -------------------------------------------------------------------------------- /MaaDownloadServer/Services/Controller/ComponentService.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using Microsoft.EntityFrameworkCore; 3 | 4 | namespace MaaDownloadServer.Services.Controller; 5 | 6 | public class ComponentService : IComponentService 7 | { 8 | private readonly MaaDownloadServerDbContext _dbContext; 9 | 10 | public ComponentService(MaaDownloadServerDbContext dbContext) 11 | { 12 | _dbContext = dbContext; 13 | } 14 | 15 | public async Task> GetAllComponents() 16 | { 17 | var componentInfosDbCache = await _dbContext.DatabaseCaches 18 | .Where(x => x.QueryId == "Component") 19 | .ToListAsync(); 20 | 21 | var componentInfos = componentInfosDbCache 22 | .Select(x => JsonSerializer.Deserialize(x.Value)) 23 | .ToList(); 24 | 25 | return componentInfos; 26 | } 27 | 28 | public async Task GetComponentDetail(string component, int limit, int page) 29 | { 30 | var allComponents = await GetAllComponents(); 31 | var componentMetaInfo = allComponents.FirstOrDefault(x => x.Name == component); 32 | 33 | if (componentMetaInfo is null) 34 | { 35 | return null; 36 | } 37 | 38 | var components = (await _dbContext.Packages 39 | .Where(x => x.Component == component) 40 | .OrderByDescending(x => x.PublishTime) 41 | .ToListAsync()) 42 | .GroupBy(x => x.Version.ToString()) 43 | .Skip((page - 1) * limit) 44 | .Take(limit) 45 | .ToList(); 46 | 47 | var versions = (from c in components 48 | let packages = c.ToList() 49 | select new ComponentVersions(c.Key, packages[0].PublishTime, packages[0].UpdateLog, 50 | packages.Select(x => new ComponentSupport 51 | (x.Platform.ToString(), x.Architecture.ToString())).ToList())) 52 | .ToList(); 53 | 54 | var dto = new GetComponentDetailDto(componentMetaInfo.Name, componentMetaInfo.Description, versions, page, limit); 55 | return dto; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /MaaDownloadServer/Services/Controller/DownloadService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Semver; 3 | 4 | namespace MaaDownloadServer.Services.Controller; 5 | 6 | public class DownloadService : IDownloadService 7 | { 8 | private readonly IFileSystemService _fileSystemService; 9 | private readonly IComponentService _componentService; 10 | private readonly MaaDownloadServerDbContext _dbContext; 11 | 12 | public DownloadService( 13 | IFileSystemService fileSystemService, 14 | IComponentService componentService, 15 | MaaDownloadServerDbContext dbContext) 16 | { 17 | _fileSystemService = fileSystemService; 18 | _componentService = componentService; 19 | _dbContext = dbContext; 20 | } 21 | 22 | public async Task GetFullPackage(string componentName, Platform platform, Architecture architecture, SemVersion version) 23 | { 24 | var tag = new PublicContentTag(PublicContentTagType.FullPackage, platform, architecture, componentName, version).ParseToTagString(); 25 | var pc = await _dbContext.PublicContents.FirstOrDefaultAsync(x => x.Tag == tag); 26 | return pc; 27 | } 28 | 29 | public async Task GetUpdatePackage(string componentName, Platform platform, Architecture architecture, SemVersion from, SemVersion to) 30 | { 31 | var allComponents = await _componentService.GetAllComponents(); 32 | var componentExist = allComponents.Exists(x => x.Name == componentName); 33 | if (componentExist is false) 34 | { 35 | return null; 36 | } 37 | 38 | if (from == to) 39 | { 40 | return null; 41 | } 42 | var tag = new PublicContentTag(PublicContentTagType.UpdatePackage, platform, architecture, componentName, from, to).ParseToTagString(); 43 | var pc = await _dbContext.PublicContents.FirstOrDefaultAsync(x => x.Tag == tag); 44 | var fromVersion = from.ToString(); 45 | var toVersion = to.ToString(); 46 | if (pc is not null) 47 | { 48 | return pc; 49 | } 50 | var fromPackage = await _dbContext.Packages 51 | .FirstOrDefaultAsync(x => x.Platform == platform && x.Architecture == architecture && x.Version == fromVersion); 52 | var toPackage = await _dbContext.Packages 53 | .FirstOrDefaultAsync(x => x.Platform == platform && x.Architecture == architecture && x.Version == toVersion); 54 | if (fromPackage is null || toPackage is null) 55 | { 56 | return null; 57 | } 58 | var diff = _fileSystemService.GetUpdateDiff(fromPackage, toPackage); 59 | var pcs = await _fileSystemService.AddUpdatePackages(componentName, new List { diff }); 60 | var npc = pcs.First(); 61 | return npc; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /MaaDownloadServer/Services/Controller/Interfaces/IComponentService.cs: -------------------------------------------------------------------------------- 1 | namespace MaaDownloadServer.Services.Controller.Interfaces; 2 | 3 | public interface IComponentService 4 | { 5 | Task> GetAllComponents(); 6 | Task GetComponentDetail(string component, int limit, int page); 7 | } 8 | -------------------------------------------------------------------------------- /MaaDownloadServer/Services/Controller/Interfaces/IDownloadService.cs: -------------------------------------------------------------------------------- 1 | using Semver; 2 | 3 | namespace MaaDownloadServer.Services.Controller.Interfaces; 4 | 5 | public interface IDownloadService 6 | { 7 | Task GetFullPackage(string componentName, Platform platform, Architecture architecture, SemVersion version); 8 | Task GetUpdatePackage(string componentName, Platform platform, Architecture architecture, SemVersion from, SemVersion to); 9 | } 10 | -------------------------------------------------------------------------------- /MaaDownloadServer/Services/Controller/Interfaces/IVersionService.cs: -------------------------------------------------------------------------------- 1 | using Semver; 2 | 3 | namespace MaaDownloadServer.Services.Controller.Interfaces; 4 | 5 | public interface IVersionService 6 | { 7 | Task GetVersion(string componentName, Platform platform, Architecture architecture, SemVersion semVersion); 8 | Task GetLatestVersion(string componentName, Platform platform, Architecture architecture); 9 | } 10 | -------------------------------------------------------------------------------- /MaaDownloadServer/Services/Controller/VersionService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Semver; 3 | 4 | namespace MaaDownloadServer.Services.Controller; 5 | 6 | public class VersionService : IVersionService 7 | { 8 | private readonly MaaDownloadServerDbContext _dbContext; 9 | 10 | public VersionService( 11 | MaaDownloadServerDbContext dbContext) 12 | { 13 | _dbContext = dbContext; 14 | } 15 | 16 | 17 | /// 18 | /// 获取对应平台和架构的某个版本 19 | /// 20 | /// 组件名 21 | /// 平台 22 | /// 架构 23 | /// 版本 24 | /// 25 | public async Task GetVersion(string componentName, Platform platform, Architecture architecture, SemVersion version) 26 | { 27 | var package = await _dbContext.Packages 28 | .Include(x => x.Resources) 29 | .Where(x => x.Component == componentName) 30 | .Where(x => x.Platform == platform && x.Architecture == architecture && x.Version == version.ToString()) 31 | .FirstOrDefaultAsync(); 32 | return package; 33 | } 34 | 35 | /// 36 | /// 获取最新版本 37 | /// 38 | /// 组件名 39 | /// 平台 40 | /// 架构 41 | /// 42 | public async Task GetLatestVersion(string componentName, Platform platform, Architecture architecture) 43 | { 44 | var package = (await _dbContext.Packages 45 | .Include(x => x.Resources) 46 | .Where(x => x.Component == componentName) 47 | .Where(x => x.Platform == platform && x.Architecture == architecture) 48 | .ToListAsync()) 49 | .OrderByDescending(x => x.Version.ParseToSemVer()) 50 | .FirstOrDefault(); 51 | return package; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /MaaDownloadServer/Utils/AttributeUtil.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace MaaDownloadServer.Utils; 4 | 5 | public static class AttributeUtil 6 | { 7 | public static string ReadAttributeValue() 8 | where TAttribute : MaaAttribute where T : class 9 | { 10 | var attr = typeof(T).GetCustomAttribute(); 11 | 12 | return attr?.GetValue(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /MaaDownloadServer/Utils/HashUtil.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography; 2 | 3 | namespace MaaDownloadServer.Utils; 4 | 5 | public static class HashUtil 6 | { 7 | public static string ComputeFileHash(ChecksumType type, string filePath) 8 | => type switch 9 | { 10 | ChecksumType.Md5 => ComputeFileMd5Hash(filePath), 11 | ChecksumType.Sha1 => ComputeFileSha1Hash(filePath), 12 | ChecksumType.Sha256 => ComputeFileSha256Hash(filePath), 13 | ChecksumType.Sha384 => ComputeFileSha384Hash(filePath), 14 | ChecksumType.Sha512 => ComputeFileSha512Hash(filePath), 15 | _ => null 16 | }; 17 | 18 | public static string ComputeFileMd5Hash(string filePath) 19 | { 20 | return ComputeFileHash(filePath); 21 | } 22 | 23 | public static string ComputeFileSha1Hash(string filePath) 24 | { 25 | return ComputeFileHash(filePath); 26 | } 27 | 28 | public static string ComputeFileSha256Hash(string filePath) 29 | { 30 | return ComputeFileHash(filePath); 31 | } 32 | 33 | public static string ComputeFileSha384Hash(string filePath) 34 | { 35 | return ComputeFileHash(filePath); 36 | } 37 | 38 | public static string ComputeFileSha512Hash(string filePath) 39 | { 40 | return ComputeFileHash(filePath); 41 | } 42 | 43 | private static string ComputeFileHash(string filePath) where T : HashAlgorithm 44 | { 45 | if (File.Exists(filePath) is false) 46 | { 47 | throw new FileNotFoundException("文件不存在", filePath); 48 | } 49 | 50 | using var fs = File.Open(filePath, FileMode.Open); 51 | using var hash = HashAlgorithm.Create(typeof(T).Name); 52 | 53 | if (hash is null) 54 | { 55 | throw new SystemException($"不支持的 Hash 算法: {typeof(T).Name}"); 56 | } 57 | 58 | var hashBytes = hash.ComputeHash(fs); 59 | var hashStr = BitConverter.ToString(hashBytes).Replace("-", ""); 60 | return hashStr; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /MaaDownloadServer/Utils/PublicContentTagUtil.cs: -------------------------------------------------------------------------------- 1 | namespace MaaDownloadServer.Utils; 2 | 3 | // Tag Example 4 | // FullPackage%%Core%%windows%%x64%%1.0.0 5 | // UpdatePackage%%Core&&windows%%x64%%1.0.0%%1.1.0 6 | 7 | public static class PublicContentTagUtil 8 | { 9 | public static PublicContentTag ParseFromTagString(this string tagString) 10 | { 11 | var tagParts = tagString.Split("%%"); 12 | if (tagParts.Length < 5) 13 | { 14 | throw new ArgumentException("Invalid tag string"); 15 | } 16 | 17 | var typeObj = Enum.Parse(tagParts[0]); 18 | var component = tagParts[1]; 19 | var platformObj = Enum.Parse(tagParts[2]); 20 | var archObj = Enum.Parse(tagParts[3]); 21 | var versionObj = tagParts[4].ParseToSemVer(); 22 | 23 | if (typeObj is PublicContentTagType.FullPackage) 24 | { 25 | return new PublicContentTag(typeObj, platformObj, archObj, component, versionObj); 26 | } 27 | 28 | if (tagParts.Length != 6) 29 | { 30 | throw new ArgumentException("Invalid tag string"); 31 | } 32 | 33 | var targetObj = tagParts[5].ParseToSemVer(); 34 | return new PublicContentTag(typeObj, platformObj, archObj, component, versionObj, targetObj); 35 | } 36 | 37 | public static string ParseToTagString(this PublicContentTag tag) 38 | { 39 | var (publicContentTagType, p, a, component, semVersion, target) = tag; 40 | if (publicContentTagType is PublicContentTagType.UpdatePackage && target is null || 41 | semVersion is null || p is Platform.UnSupported || a is Architecture.UnSupported) 42 | { 43 | throw new ArgumentException("Invalid tag"); 44 | } 45 | return publicContentTagType is PublicContentTagType.FullPackage ? 46 | $"{publicContentTagType}%%{component}%%{p}%%{a}%%{semVersion}" : 47 | $"{publicContentTagType}%%{component}%%{p}%%{a}%%{semVersion}%%{target}"; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /MaaDownloadServer/appsettings.Docker.json: -------------------------------------------------------------------------------- 1 | { 2 | "AllowedHosts": "*", 3 | "Serilog": { 4 | "Using": [ 5 | "Serilog.Sinks.Console", 6 | "Serilog.Sinks.File", 7 | "Serilog.Sinks.Grafana.Loki" 8 | ], 9 | "MinimumLevel": { 10 | "Default": "Information", 11 | "Override": { 12 | "Microsoft": "Warning", 13 | "Microsoft.Hosting.Lifetime": "Information", 14 | "Microsoft.EntityFrameworkCore.Database.Command": "Warning" 15 | } 16 | }, 17 | "WriteTo": [ 18 | { 19 | "Name": "Console", 20 | "Args": { 21 | "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {ThreadId} {Message:lj}{NewLine}{Exception}" 22 | } 23 | }, 24 | { 25 | "Name": "File", 26 | "Args": { 27 | "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {ThreadId} {Message:lj}{NewLine}{Exception}", 28 | "path": "{{DATA DIRECTORY}}/logs/log-.log", 29 | "rollingInterval": "Day", 30 | "shared": true 31 | } 32 | }, 33 | { 34 | "Name": "GrafanaLoki", 35 | "Args": { 36 | "uri": "{GRAFANA LOKI URL}", 37 | "labels": [ 38 | { 39 | "key": "app", 40 | "value": "maa_download_server" 41 | } 42 | ], 43 | "filtrationMode": "Include", 44 | "filtrationLabels": [ 45 | "X-Trace-Id" 46 | ], 47 | "outputTemplate": "{Timestamp:dd-MM-yyyy HH:mm:ss} [{Level:u3}] [{ThreadId}] {Message}{NewLine}{Exception}", 48 | "textFormatter": "Serilog.Sinks.Grafana.Loki.LokiJsonTextFormatter, Serilog.Sinks.Grafana.Loki" 49 | } 50 | } 51 | ], 52 | "Enrich": [ 53 | "FromLogContext", 54 | "WithMachineName", 55 | "WithThreadId" 56 | ] 57 | }, 58 | "MaaServer": { 59 | "Server": { 60 | "Host": "*", 61 | "Port": 5089, 62 | "ApiFullUrl": "http://localhost:5089" 63 | }, 64 | "Network": { 65 | "Proxy": "", 66 | "UserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36 Edg/99.0.1150.39" 67 | }, 68 | "GameData": { 69 | "UpdateJobInterval": 120 70 | }, 71 | "DataDirectories": { 72 | "RootPath": "{{DATA DIRECTORY}}", 73 | "SubDirectories": { 74 | "Downloads": "downloads", 75 | "Public": "public", 76 | "Resources": "resources", 77 | "Database": "database", 78 | "Temp": "temp", 79 | "Scripts": "scripts", 80 | "Static": "static", 81 | "VirtualEnvironments": "venvs" 82 | } 83 | }, 84 | "PublicContent": { 85 | "OutdatedCheckInterval": 60, 86 | "DefaultDuration": 30, 87 | "AutoBundledDuration": 60 88 | }, 89 | "ScriptEngine": { 90 | "Python": "/usr/bin/python3" 91 | }, 92 | "Announce": { 93 | "ServerChanSendKeys": [] 94 | } 95 | }, 96 | "IpRateLimiting": { 97 | "EnableEndpointRateLimiting": false, 98 | "StackBlockedRequests": false, 99 | "RealIpHeader": "X-Real-IP", 100 | "HttpStatusCode": 429, 101 | "GeneralRules": [ 102 | { 103 | "Endpoint": "*", 104 | "Period": "1m", 105 | "Limit": 45 106 | }, 107 | { 108 | "Endpoint": "*", 109 | "Period": "1h", 110 | "Limit": 1800 111 | } 112 | ] 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /MaaDownloadServer/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "AllowedHosts": "*", 3 | "Serilog": { 4 | "Using": [ 5 | "Serilog.Sinks.Console", 6 | "Serilog.Sinks.File", 7 | "Serilog.Sinks.Grafana.Loki" 8 | ], 9 | "MinimumLevel": { 10 | "Default": "Information", 11 | "Override": { 12 | "Microsoft": "Warning", 13 | "Microsoft.Hosting.Lifetime": "Information", 14 | "Microsoft.EntityFrameworkCore.Database.Command": "Warning" 15 | } 16 | }, 17 | "WriteTo": [ 18 | { 19 | "Name": "Console", 20 | "Args": { 21 | "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {ThreadId} {Message:lj}{NewLine}{Exception}" 22 | } 23 | }, 24 | { 25 | "Name": "File", 26 | "Args": { 27 | "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {ThreadId} {Message:lj}{NewLine}{Exception}", 28 | "path": "{{DATA DIRECTORY}}/logs/log-.log", 29 | "rollingInterval": "Day", 30 | "shared": true 31 | } 32 | }, 33 | { 34 | "Name": "GrafanaLoki", 35 | "Args": { 36 | "uri": "{GRAFANA LOKI URL}", 37 | "labels": [ 38 | { 39 | "key": "app", 40 | "value": "maa_download_server" 41 | } 42 | ], 43 | "filtrationMode": "Include", 44 | "filtrationLabels": [ 45 | "X-Trace-Id" 46 | ], 47 | "outputTemplate": "{Timestamp:dd-MM-yyyy HH:mm:ss} [{Level:u3}] [{ThreadId}] {Message}{NewLine}{Exception}", 48 | "textFormatter": "Serilog.Sinks.Grafana.Loki.LokiJsonTextFormatter, Serilog.Sinks.Grafana.Loki" 49 | } 50 | } 51 | ], 52 | "Enrich": [ 53 | "FromLogContext", 54 | "WithMachineName", 55 | "WithThreadId" 56 | ] 57 | }, 58 | "MaaServer": { 59 | "Server": { 60 | "Host": "*", 61 | "Port": 5089, 62 | "ApiFullUrl": "http://localhost:5089" 63 | }, 64 | "Network": { 65 | "Proxy": "", 66 | "UserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36 Edg/99.0.1150.39" 67 | }, 68 | "DataDirectories": { 69 | "RootPath": "{{DATA DIRECTORY}}", 70 | "SubDirectories": { 71 | "Downloads": "downloads", 72 | "Public": "public", 73 | "Resources": "resources", 74 | "Database": "database", 75 | "Temp": "temp", 76 | "Scripts": "scripts", 77 | "Static": "static", 78 | "VirtualEnvironments": "venvs" 79 | } 80 | }, 81 | "PublicContent": { 82 | "OutdatedCheckInterval": 60, 83 | "DefaultDuration": 30, 84 | "AutoBundledDuration": 60 85 | }, 86 | "ScriptEngine": { 87 | "Python": "BASE PYTHON INTERPRETER PATH" 88 | }, 89 | "Announce": { 90 | "ServerChanSendKeys": [] 91 | } 92 | }, 93 | "IpRateLimiting": { 94 | "EnableEndpointRateLimiting": false, 95 | "StackBlockedRequests": false, 96 | "RealIpHeader": "X-Real-IP", 97 | "HttpStatusCode": 429, 98 | "GeneralRules": [ 99 | { 100 | "Endpoint": "*", 101 | "Period": "1m", 102 | "Limit": 45 103 | }, 104 | { 105 | "Endpoint": "*", 106 | "Period": "1h", 107 | "Limit": 1800 108 | } 109 | ] 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MaaDownloadServer 2 | 3 | ![GitHub Workflow Status](https://img.shields.io/github/workflow/status/MaaAssistantArknights/MaaDownloadServer/build-test?label=CI%3Abuild-test&logo=github) 4 | ![GitHub Workflow Status](https://img.shields.io/github/workflow/status/MaaAssistantArknights/MaaDownloadServer/publish-docker?label=CI%3Apublish-docker&logo=github) 5 | ![C Sharp](https://img.shields.io/badge/C%23-10-239120?logo=csharp) 6 | ![.NET](https://img.shields.io/badge/.NET-6.0-512BD4?logo=.net) 7 | ![GitHub](https://img.shields.io/github/license/MaaAssistantArknights/MaaDownloadServer) 8 | ![GitHub commit activity](https://img.shields.io/github/commit-activity/m/MaaAssistantArknights/MaaDownloadServer) 9 | 10 | ![Docker Image Version (latest by date)](https://img.shields.io/docker/v/alisaqaq/maa-download-server?arch=amd64&label=Docker%20Image%20%28amd64%29&logo=docker) 11 | ![Docker Image Version (latest by date)](https://img.shields.io/docker/v/alisaqaq/maa-download-server?arch=arm64&label=Docker%20Image%20%28arm64%29&logo=docker) 12 | ![Docker Image Version (latest by date)](https://img.shields.io/docker/v/alisaqaq/maa-download-server?arch=arm&label=Docker%20Image%20%28arm%2Fv7%29&logo=docker) 13 | 14 | ## 关于 15 | 16 | MAA 更新和下载服务器 17 | 18 | 用于 MAA 本体在国内的下载加速,以及提供增量更新包 19 | 20 | ## 功能 21 | 22 | * 提供 API 用于检查不同平台和架构所支持的 MAA 版本 23 | * 提供 MAA 本体完整包的下载 24 | * 提供 MAA 增量更新包的下载 25 | 26 | 27 | ## 文档 28 | 29 | * [API 文档 (ApiFox)](https://www.apifox.cn/apidoc/shared-e9acdf71-e5e6-4198-aaa7-5417e1304335) 30 | * [编译和打包](./docs/Compile.md) 31 | * [本地运行配置](./docs/RunNative.md) 32 | * [Docker 运行配置](./docs/RunDocker.md) 33 | * [Component 和 Python 脚本](./docs/ComponentAndPythonScript.md) 34 | 35 | # 致谢 36 | 37 | 感谢本项目使用到的开源项目/开源库: 38 | 39 | - 框架:[ASP.NET Core](https://github.com/dotnet/aspnetcore) ![GitHub](https://img.shields.io/github/license/dotnet/aspnetcore) 40 | - JSON 字符串处理:[EscapeRoute](https://github.com/JackWFinlay/EscapeRoute) ![GitHub](https://img.shields.io/github/license/JackWFinlay/EscapeRoute) 41 | - 限流:[AspNetCoreRateLimit](https://github.com/stefanprodan/AspNetCoreRateLimit) ![GitHub](https://img.shields.io/github/license/stefanprodan/AspNetCoreRateLimit) 42 | - 计划任务:[Quartz.NET](https://github.com/quartznet/quartznet) ![GitHub](https://img.shields.io/github/license/quartznet/quartznet) 43 | - 版本号解析:[Semver](https://github.com/maxhauser/semver) ![GitHub](https://img.shields.io/github/license/maxhauser/semver) 44 | - 日志记录:[Serilog.AspNetCore](https://github.com/serilog/serilog-aspnetcore) ![GitHub](https://img.shields.io/github/license/serilog/serilog-aspnetcore) 45 | - ORM:[Entity Framework Core](https://github.com/dotnet/efcore) ![GitHub](https://img.shields.io/github/license/dotnet/efcore) 46 | - 自动构建:[Cake.Frosting](https://github.com/cake-build/cake) ![GitHub](https://img.shields.io/github/license/cake-build/cake) 47 | 48 | # 许可证 49 | 50 | 本项目使用 [GNU AFFERO GENERAL PUBLIC LICENSE Version 3](./LICENSE) 授权 51 | -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | # Script Demo 2 | 3 | 本示例已经投入使用。 4 | 5 | [component.json](./component.json) 文件与 [get_download_info.py](./get_download_info.py) 文件都应放在 `data/scripts/MaaCore` 目录下。 6 | -------------------------------------------------------------------------------- /demo/component.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MaaCore", 3 | "description": "MaaAssistantArknights 核心组件", 4 | "metadata_urls": [ 5 | "https://api.github.com/repos/MaaAssistantArknights/MaaRelease/releases?per_page=3&page=1" 6 | ], 7 | "default_url_placeholder": {}, 8 | "after_download_process": { 9 | "operation": "Unzip", 10 | "args": {} 11 | }, 12 | "before_add_process": { 13 | "operation": "Zip", 14 | "args": { 15 | "zip": [ 16 | { 17 | "name": "resource", 18 | "files": [], 19 | "folders": [ 20 | "resource" 21 | ] 22 | } 23 | ] 24 | } 25 | }, 26 | "scripts": { 27 | "get_download_info": "get_download_info.py", 28 | "after_download_process": "", 29 | "before_add_process": "", 30 | "relative_path_calculation": "" 31 | }, 32 | "use_proxy": false, 33 | "pack_update_package": true, 34 | "interval": 10 35 | } 36 | -------------------------------------------------------------------------------- /demo/get_download_info.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import json 3 | import re 4 | 5 | if len(sys.argv) != 3: 6 | sys.stderr.write("参数不正确, 当前参数共" + str(len(sys.argv)) + 7 | "个, 为: " + str(sys.argv) + "\n") 8 | 9 | config_json_string = sys.argv[1] 10 | json_string = sys.argv[2] 11 | 12 | try: 13 | json_obj = json.loads(json_string) 14 | config = json.loads(config_json_string) 15 | 16 | infos = [] 17 | 18 | for release_info in json_obj: 19 | version: str = release_info["tag_name"] 20 | version = version.removeprefix("v") 21 | publish_time = release_info["published_at"] 22 | update_log = release_info["body"] 23 | 24 | infoContent = {} 25 | 26 | infoContent["version"] = version 27 | infoContent["update_time"] = publish_time 28 | infoContent["update_log"] = update_log 29 | 30 | regex = "MaaCore-(Windows|MacOS|Linux)-(x64|arm64)-v((0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?).zip" 31 | 32 | for asst in release_info["assets"]: 33 | 34 | packageInfo = infoContent.copy() 35 | 36 | download_url = asst["browser_download_url"] 37 | file_name = asst["name"] 38 | 39 | if re.fullmatch(regex, file_name) is None: 40 | continue 41 | 42 | info_string = file_name.replace("MaaCore-", "") 43 | splitted = info_string.split("-") 44 | platform_string = splitted[0] 45 | arch_string = splitted[1] 46 | 47 | packageInfo["platform"] = platform_string 48 | packageInfo["arch"] = arch_string 49 | packageInfo["download_url"] = download_url 50 | packageInfo["file_extension"] = "zip" 51 | packageInfo["checksum"] = "" 52 | packageInfo["checksum_type"] = "none" 53 | 54 | infos.append(packageInfo) 55 | 56 | json_output = json.dumps(infos) 57 | 58 | sys.stdout.write(json_output) 59 | 60 | except Exception as e: 61 | sys.stderr.write(str(e) + "\n") 62 | -------------------------------------------------------------------------------- /docs/Compile.md: -------------------------------------------------------------------------------- 1 | # 编译和打包 2 | 3 | ## 发布到本机 4 | 5 | ### 必要条件 6 | 7 | * [.NET SDK 6.0.*](https://dotnet.microsoft.com/en-us/download/dotnet) 8 | 9 | ### 步骤 10 | 11 | * 安装 .NET SDK 6.0 12 | * 用任何方法把这个仓库里的代码弄到本地 13 | * 打开命令行 (CMD/PowerShell/Bash...) 14 | * CD 到项目目录 15 | * 执行 `publish.sh` 或者 `publish.ps1` 文件,参数见下面 16 | * Publish 文件在 `./publish` 目录下,同时会打包为 `./MaaDownloadServer-{Configuration}-{Framework}-{RID}.zip` 文件 17 | 18 | ### Publish 参数 19 | 20 | 运行格式:`./publish.sh [options]` 或者 `./publish.ps1 [options]` 21 | 22 | Options 格式:`--