├── .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