├── .github
└── workflows
│ ├── action.yaml
│ └── manual.yaml
├── .gitignore
├── LICENSE
├── README.md
├── build.gradle.kts
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle.kts
└── src
├── main
├── kotlin
│ └── com
│ │ └── github
│ │ └── fastmirrorserver
│ │ ├── Application.kt
│ │ ├── annotation
│ │ ├── Authority.kt
│ │ └── RawResponse.kt
│ │ ├── component
│ │ ├── Cache.kt
│ │ └── MemoryCache.kt
│ │ ├── config
│ │ ├── DataSourceInitializer.kt
│ │ ├── InterceptorConfiguration.kt
│ │ └── KtOrmConfiguration.kt
│ │ ├── controller
│ │ ├── AccountController.kt
│ │ ├── ApiExceptionHandler.kt
│ │ ├── ApiResponseHandler.kt
│ │ ├── HttpErrorController.kt
│ │ ├── QueryController.kt
│ │ ├── TestController.kt
│ │ ├── TracebackController.kt
│ │ └── UploadTaskController.kt
│ │ ├── dto
│ │ ├── ApiResponse.kt
│ │ ├── Metadata.kt
│ │ ├── Summary.kt
│ │ └── Traceback.kt
│ │ ├── entity
│ │ ├── Account.kt
│ │ ├── Manifest.kt
│ │ └── Project.kt
│ │ ├── exception
│ │ └── ApiException.kt
│ │ ├── interceptor
│ │ ├── AuthorizationInterceptor.kt
│ │ └── ResponseResultInterceptor.kt
│ │ ├── pojo
│ │ ├── AccountPOJO.kt
│ │ └── ManifestPOJO.kt
│ │ ├── service
│ │ ├── AuthorizationService.kt
│ │ ├── ErrorReportService.kt
│ │ ├── FileService.kt
│ │ └── UploadTaskService.kt
│ │ └── util
│ │ ├── Crypto.kt
│ │ ├── Database.kt
│ │ ├── FileWriter.kt
│ │ ├── Json.kt
│ │ ├── Network.kt
│ │ ├── Time.kt
│ │ ├── UploadTaskContainer.kt
│ │ ├── Utility.kt
│ │ └── enums
│ │ └── Permission.kt
└── resources
│ ├── application.yml
│ └── table.sql
└── test
├── kotlin
└── com
│ └── github
│ └── fastmirrorserver
│ └── test
│ ├── AccountTest.kt
│ └── UtilityTest.kt
└── resources
├── application.yml
└── table.sql
/.github/workflows/action.yaml:
--------------------------------------------------------------------------------
1 | name: CI Automatic
2 | on:
3 | push:
4 | tags-ignore:
5 | - ref/*
6 | jobs:
7 | build:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: actions/checkout@v2
11 | - name: Setup JDK11
12 | uses: actions/setup-java@v2
13 | with:
14 | java-version: '11'
15 | distribution: 'adopt'
16 | - uses: gradle/wrapper-validation-action@v1
17 | - run: chmod +x ./gradlew
18 | - name: Build
19 | uses: gradle/gradle-build-action@v2
20 | with:
21 | arguments: bootJar
22 | - name: Create Release
23 | id: create_release
24 | uses: actions/create-release@latest
25 | env:
26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
27 | with:
28 | tag_name: ${{ github.ref }}
29 | release_name: ${{ github.ref }}
30 | prerelease: false
31 | draft: false
32 | - name: Upload Assets
33 | uses: actions/upload-release-asset@latest
34 | env:
35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
36 | with:
37 | upload_url: ${{ steps.create_release.outputs.upload_url }}
38 | asset_path: build/libs/server.jar
39 | asset_name: FastMirrorServer.jar
40 | asset_content_type: application/jar
41 |
--------------------------------------------------------------------------------
/.github/workflows/manual.yaml:
--------------------------------------------------------------------------------
1 | name: CI Manual
2 | on:
3 | workflow_dispatch:
4 | inputs:
5 | publish:
6 | description: is release publish
7 | required: true
8 | type: choice
9 | options:
10 | - release
11 | - pre-release
12 | default: release
13 | jobs:
14 | build:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: actions/checkout@v2
18 | - name: Setup JDK11
19 | uses: actions/setup-java@v2
20 | with:
21 | java-version: '11'
22 | distribution: 'adopt'
23 | - uses: gradle/wrapper-validation-action@v1
24 | - run: chmod +x ./gradlew
25 | - name: Build
26 | uses: gradle/gradle-build-action@v2
27 | with:
28 | arguments: bootJar
29 | - name: Create Release
30 | id: create_release
31 | uses: actions/create-release@latest
32 | env:
33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
34 | with:
35 | tag_name: ${{ github.ref }}
36 | release_name: ${{ github.ref }}
37 | prerelease: ${{ github.event.inputs.version-suffix != 'release' }}
38 | draft: false
39 | - name: Upload Assets
40 | uses: actions/upload-release-asset@latest
41 | env:
42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
43 | with:
44 | upload_url: ${{ steps.create_release.outputs.upload_url }}
45 | asset_path: build/libs/server.jar
46 | asset_name: FastMirrorServer.jar
47 | asset_content_type: application/jar
48 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Ignore Gradle GUI config
2 | gradle-app.setting
3 |
4 | # Cache of project
5 | .gradletasknamecache
6 |
7 | # idea
8 | .idea
9 |
10 | # Compiled class file
11 | *.class
12 |
13 | # Log file
14 | *.log
15 |
16 | # Package Files #
17 | *.jar
18 | *.war
19 | *.nar
20 | *.ear
21 | *.zip
22 | *.tar.gz
23 | *.rar
24 |
25 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
26 | !gradle-wrapper.jar
27 |
28 | .gradle
29 | **/build/
30 | !src/**/build/
31 |
32 |
33 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
34 | hs_err_pid*
35 | *.xml
36 | .idea/workspace.xml
37 | .idea.*
38 | javadocs/*
39 | target/*
40 | *.sh
41 | src/test/resources/testCredentials.json
42 | *.txt
43 | target/maven-archiver/pom.properties
44 |
45 | # project
46 | core/
47 | tmp/
48 | error-reports/
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU AFFERO GENERAL PUBLIC LICENSE
2 | Version 3, 19 November 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU Affero General Public License is a free, copyleft license for
11 | software and other kinds of works, specifically designed to ensure
12 | cooperation with the community in the case of network server software.
13 |
14 | The licenses for most software and other practical works are designed
15 | to take away your freedom to share and change the works. By contrast,
16 | our General Public Licenses are intended to guarantee your freedom to
17 | share and change all versions of a program--to make sure it remains free
18 | software for all its users.
19 |
20 | When we speak of free software, we are referring to freedom, not
21 | price. Our General Public Licenses are designed to make sure that you
22 | have the freedom to distribute copies of free software (and charge for
23 | them if you wish), that you receive source code or can get it if you
24 | want it, that you can change the software or use pieces of it in new
25 | free programs, and that you know you can do these things.
26 |
27 | Developers that use our General Public Licenses protect your rights
28 | with two steps: (1) assert copyright on the software, and (2) offer
29 | you this License which gives you legal permission to copy, distribute
30 | and/or modify the software.
31 |
32 | A secondary benefit of defending all users' freedom is that
33 | improvements made in alternate versions of the program, if they
34 | receive widespread use, become available for other developers to
35 | incorporate. Many developers of free software are heartened and
36 | encouraged by the resulting cooperation. However, in the case of
37 | software used on network servers, this result may fail to come about.
38 | The GNU General Public License permits making a modified version and
39 | letting the public access it on a server without ever releasing its
40 | source code to the public.
41 |
42 | The GNU Affero General Public License is designed specifically to
43 | ensure that, in such cases, the modified source code becomes available
44 | to the community. It requires the operator of a network server to
45 | provide the source code of the modified version running there to the
46 | users of that server. Therefore, public use of a modified version, on
47 | a publicly accessible server, gives the public access to the source
48 | code of the modified version.
49 |
50 | An older license, called the Affero General Public License and
51 | published by Affero, was designed to accomplish similar goals. This is
52 | a different license, not a version of the Affero GPL, but Affero has
53 | released a new version of the Affero GPL which permits relicensing under
54 | this license.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | TERMS AND CONDITIONS
60 |
61 | 0. Definitions.
62 |
63 | "This License" refers to version 3 of the GNU Affero General Public License.
64 |
65 | "Copyright" also means copyright-like laws that apply to other kinds of
66 | works, such as semiconductor masks.
67 |
68 | "The Program" refers to any copyrightable work licensed under this
69 | License. Each licensee is addressed as "you". "Licensees" and
70 | "recipients" may be individuals or organizations.
71 |
72 | To "modify" a work means to copy from or adapt all or part of the work
73 | in a fashion requiring copyright permission, other than the making of an
74 | exact copy. The resulting work is called a "modified version" of the
75 | earlier work or a work "based on" the earlier work.
76 |
77 | A "covered work" means either the unmodified Program or a work based
78 | on the Program.
79 |
80 | To "propagate" a work means to do anything with it that, without
81 | permission, would make you directly or secondarily liable for
82 | infringement under applicable copyright law, except executing it on a
83 | computer or modifying a private copy. Propagation includes copying,
84 | distribution (with or without modification), making available to the
85 | public, and in some countries other activities as well.
86 |
87 | To "convey" a work means any kind of propagation that enables other
88 | parties to make or receive copies. Mere interaction with a user through
89 | a computer network, with no transfer of a copy, is not conveying.
90 |
91 | An interactive user interface displays "Appropriate Legal Notices"
92 | to the extent that it includes a convenient and prominently visible
93 | feature that (1) displays an appropriate copyright notice, and (2)
94 | tells the user that there is no warranty for the work (except to the
95 | extent that warranties are provided), that licensees may convey the
96 | work under this License, and how to view a copy of this License. If
97 | the interface presents a list of user commands or options, such as a
98 | menu, a prominent item in the list meets this criterion.
99 |
100 | 1. Source Code.
101 |
102 | The "source code" for a work means the preferred form of the work
103 | for making modifications to it. "Object code" means any non-source
104 | form of a work.
105 |
106 | A "Standard Interface" means an interface that either is an official
107 | standard defined by a recognized standards body, or, in the case of
108 | interfaces specified for a particular programming language, one that
109 | is widely used among developers working in that language.
110 |
111 | The "System Libraries" of an executable work include anything, other
112 | than the work as a whole, that (a) is included in the normal form of
113 | packaging a Major Component, but which is not part of that Major
114 | Component, and (b) serves only to enable use of the work with that
115 | Major Component, or to implement a Standard Interface for which an
116 | implementation is available to the public in source code form. A
117 | "Major Component", in this context, means a major essential component
118 | (kernel, window system, and so on) of the specific operating system
119 | (if any) on which the executable work runs, or a compiler used to
120 | produce the work, or an object code interpreter used to run it.
121 |
122 | The "Corresponding Source" for a work in object code form means all
123 | the source code needed to generate, install, and (for an executable
124 | work) run the object code and to modify the work, including scripts to
125 | control those activities. However, it does not include the work's
126 | System Libraries, or general-purpose tools or generally available free
127 | programs which are used unmodified in performing those activities but
128 | which are not part of the work. For example, Corresponding Source
129 | includes interface definition files associated with source files for
130 | the work, and the source code for shared libraries and dynamically
131 | linked subprograms that the work is specifically designed to require,
132 | such as by intimate data communication or control flow between those
133 | subprograms and other parts of the work.
134 |
135 | The Corresponding Source need not include anything that users
136 | can regenerate automatically from other parts of the Corresponding
137 | Source.
138 |
139 | The Corresponding Source for a work in source code form is that
140 | same work.
141 |
142 | 2. Basic Permissions.
143 |
144 | All rights granted under this License are granted for the term of
145 | copyright on the Program, and are irrevocable provided the stated
146 | conditions are met. This License explicitly affirms your unlimited
147 | permission to run the unmodified Program. The output from running a
148 | covered work is covered by this License only if the output, given its
149 | content, constitutes a covered work. This License acknowledges your
150 | rights of fair use or other equivalent, as provided by copyright law.
151 |
152 | You may make, run and propagate covered works that you do not
153 | convey, without conditions so long as your license otherwise remains
154 | in force. You may convey covered works to others for the sole purpose
155 | of having them make modifications exclusively for you, or provide you
156 | with facilities for running those works, provided that you comply with
157 | the terms of this License in conveying all material for which you do
158 | not control copyright. Those thus making or running the covered works
159 | for you must do so exclusively on your behalf, under your direction
160 | and control, on terms that prohibit them from making any copies of
161 | your copyrighted material outside their relationship with you.
162 |
163 | Conveying under any other circumstances is permitted solely under
164 | the conditions stated below. Sublicensing is not allowed; section 10
165 | makes it unnecessary.
166 |
167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
168 |
169 | No covered work shall be deemed part of an effective technological
170 | measure under any applicable law fulfilling obligations under article
171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
172 | similar laws prohibiting or restricting circumvention of such
173 | measures.
174 |
175 | When you convey a covered work, you waive any legal power to forbid
176 | circumvention of technological measures to the extent such circumvention
177 | is effected by exercising rights under this License with respect to
178 | the covered work, and you disclaim any intention to limit operation or
179 | modification of the work as a means of enforcing, against the work's
180 | users, your or third parties' legal rights to forbid circumvention of
181 | technological measures.
182 |
183 | 4. Conveying Verbatim Copies.
184 |
185 | You may convey verbatim copies of the Program's source code as you
186 | receive it, in any medium, provided that you conspicuously and
187 | appropriately publish on each copy an appropriate copyright notice;
188 | keep intact all notices stating that this License and any
189 | non-permissive terms added in accord with section 7 apply to the code;
190 | keep intact all notices of the absence of any warranty; and give all
191 | recipients a copy of this License along with the Program.
192 |
193 | You may charge any price or no price for each copy that you convey,
194 | and you may offer support or warranty protection for a fee.
195 |
196 | 5. Conveying Modified Source Versions.
197 |
198 | You may convey a work based on the Program, or the modifications to
199 | produce it from the Program, in the form of source code under the
200 | terms of section 4, provided that you also meet all of these conditions:
201 |
202 | a) The work must carry prominent notices stating that you modified
203 | it, and giving a relevant date.
204 |
205 | b) The work must carry prominent notices stating that it is
206 | released under this License and any conditions added under section
207 | 7. This requirement modifies the requirement in section 4 to
208 | "keep intact all notices".
209 |
210 | c) You must license the entire work, as a whole, under this
211 | License to anyone who comes into possession of a copy. This
212 | License will therefore apply, along with any applicable section 7
213 | additional terms, to the whole of the work, and all its parts,
214 | regardless of how they are packaged. This License gives no
215 | permission to license the work in any other way, but it does not
216 | invalidate such permission if you have separately received it.
217 |
218 | d) If the work has interactive user interfaces, each must display
219 | Appropriate Legal Notices; however, if the Program has interactive
220 | interfaces that do not display Appropriate Legal Notices, your
221 | work need not make them do so.
222 |
223 | A compilation of a covered work with other separate and independent
224 | works, which are not by their nature extensions of the covered work,
225 | and which are not combined with it such as to form a larger program,
226 | in or on a volume of a storage or distribution medium, is called an
227 | "aggregate" if the compilation and its resulting copyright are not
228 | used to limit the access or legal rights of the compilation's users
229 | beyond what the individual works permit. Inclusion of a covered work
230 | in an aggregate does not cause this License to apply to the other
231 | parts of the aggregate.
232 |
233 | 6. Conveying Non-Source Forms.
234 |
235 | You may convey a covered work in object code form under the terms
236 | of sections 4 and 5, provided that you also convey the
237 | machine-readable Corresponding Source under the terms of this License,
238 | in one of these ways:
239 |
240 | a) Convey the object code in, or embodied in, a physical product
241 | (including a physical distribution medium), accompanied by the
242 | Corresponding Source fixed on a durable physical medium
243 | customarily used for software interchange.
244 |
245 | b) Convey the object code in, or embodied in, a physical product
246 | (including a physical distribution medium), accompanied by a
247 | written offer, valid for at least three years and valid for as
248 | long as you offer spare parts or customer support for that product
249 | model, to give anyone who possesses the object code either (1) a
250 | copy of the Corresponding Source for all the software in the
251 | product that is covered by this License, on a durable physical
252 | medium customarily used for software interchange, for a price no
253 | more than your reasonable cost of physically performing this
254 | conveying of source, or (2) access to copy the
255 | Corresponding Source from a network server at no charge.
256 |
257 | c) Convey individual copies of the object code with a copy of the
258 | written offer to provide the Corresponding Source. This
259 | alternative is allowed only occasionally and noncommercially, and
260 | only if you received the object code with such an offer, in accord
261 | with subsection 6b.
262 |
263 | d) Convey the object code by offering access from a designated
264 | place (gratis or for a charge), and offer equivalent access to the
265 | Corresponding Source in the same way through the same place at no
266 | further charge. You need not require recipients to copy the
267 | Corresponding Source along with the object code. If the place to
268 | copy the object code is a network server, the Corresponding Source
269 | may be on a different server (operated by you or a third party)
270 | that supports equivalent copying facilities, provided you maintain
271 | clear directions next to the object code saying where to find the
272 | Corresponding Source. Regardless of what server hosts the
273 | Corresponding Source, you remain obligated to ensure that it is
274 | available for as long as needed to satisfy these requirements.
275 |
276 | e) Convey the object code using peer-to-peer transmission, provided
277 | you inform other peers where the object code and Corresponding
278 | Source of the work are being offered to the general public at no
279 | charge under subsection 6d.
280 |
281 | A separable portion of the object code, whose source code is excluded
282 | from the Corresponding Source as a System Library, need not be
283 | included in conveying the object code work.
284 |
285 | A "User Product" is either (1) a "consumer product", which means any
286 | tangible personal property which is normally used for personal, family,
287 | or household purposes, or (2) anything designed or sold for incorporation
288 | into a dwelling. In determining whether a product is a consumer product,
289 | doubtful cases shall be resolved in favor of coverage. For a particular
290 | product received by a particular user, "normally used" refers to a
291 | typical or common use of that class of product, regardless of the status
292 | of the particular user or of the way in which the particular user
293 | actually uses, or expects or is expected to use, the product. A product
294 | is a consumer product regardless of whether the product has substantial
295 | commercial, industrial or non-consumer uses, unless such uses represent
296 | the only significant mode of use of the product.
297 |
298 | "Installation Information" for a User Product means any methods,
299 | procedures, authorization keys, or other information required to install
300 | and execute modified versions of a covered work in that User Product from
301 | a modified version of its Corresponding Source. The information must
302 | suffice to ensure that the continued functioning of the modified object
303 | code is in no case prevented or interfered with solely because
304 | modification has been made.
305 |
306 | If you convey an object code work under this section in, or with, or
307 | specifically for use in, a User Product, and the conveying occurs as
308 | part of a transaction in which the right of possession and use of the
309 | User Product is transferred to the recipient in perpetuity or for a
310 | fixed term (regardless of how the transaction is characterized), the
311 | Corresponding Source conveyed under this section must be accompanied
312 | by the Installation Information. But this requirement does not apply
313 | if neither you nor any third party retains the ability to install
314 | modified object code on the User Product (for example, the work has
315 | been installed in ROM).
316 |
317 | The requirement to provide Installation Information does not include a
318 | requirement to continue to provide support service, warranty, or updates
319 | for a work that has been modified or installed by the recipient, or for
320 | the User Product in which it has been modified or installed. Access to a
321 | network may be denied when the modification itself materially and
322 | adversely affects the operation of the network or violates the rules and
323 | protocols for communication across the network.
324 |
325 | Corresponding Source conveyed, and Installation Information provided,
326 | in accord with this section must be in a format that is publicly
327 | documented (and with an implementation available to the public in
328 | source code form), and must require no special password or key for
329 | unpacking, reading or copying.
330 |
331 | 7. Additional Terms.
332 |
333 | "Additional permissions" are terms that supplement the terms of this
334 | License by making exceptions from one or more of its conditions.
335 | Additional permissions that are applicable to the entire Program shall
336 | be treated as though they were included in this License, to the extent
337 | that they are valid under applicable law. If additional permissions
338 | apply only to part of the Program, that part may be used separately
339 | under those permissions, but the entire Program remains governed by
340 | this License without regard to the additional permissions.
341 |
342 | When you convey a copy of a covered work, you may at your option
343 | remove any additional permissions from that copy, or from any part of
344 | it. (Additional permissions may be written to require their own
345 | removal in certain cases when you modify the work.) You may place
346 | additional permissions on material, added by you to a covered work,
347 | for which you have or can give appropriate copyright permission.
348 |
349 | Notwithstanding any other provision of this License, for material you
350 | add to a covered work, you may (if authorized by the copyright holders of
351 | that material) supplement the terms of this License with terms:
352 |
353 | a) Disclaiming warranty or limiting liability differently from the
354 | terms of sections 15 and 16 of this License; or
355 |
356 | b) Requiring preservation of specified reasonable legal notices or
357 | author attributions in that material or in the Appropriate Legal
358 | Notices displayed by works containing it; or
359 |
360 | c) Prohibiting misrepresentation of the origin of that material, or
361 | requiring that modified versions of such material be marked in
362 | reasonable ways as different from the original version; or
363 |
364 | d) Limiting the use for publicity purposes of names of licensors or
365 | authors of the material; or
366 |
367 | e) Declining to grant rights under trademark law for use of some
368 | trade names, trademarks, or service marks; or
369 |
370 | f) Requiring indemnification of licensors and authors of that
371 | material by anyone who conveys the material (or modified versions of
372 | it) with contractual assumptions of liability to the recipient, for
373 | any liability that these contractual assumptions directly impose on
374 | those licensors and authors.
375 |
376 | All other non-permissive additional terms are considered "further
377 | restrictions" within the meaning of section 10. If the Program as you
378 | received it, or any part of it, contains a notice stating that it is
379 | governed by this License along with a term that is a further
380 | restriction, you may remove that term. If a license document contains
381 | a further restriction but permits relicensing or conveying under this
382 | License, you may add to a covered work material governed by the terms
383 | of that license document, provided that the further restriction does
384 | not survive such relicensing or conveying.
385 |
386 | If you add terms to a covered work in accord with this section, you
387 | must place, in the relevant source files, a statement of the
388 | additional terms that apply to those files, or a notice indicating
389 | where to find the applicable terms.
390 |
391 | Additional terms, permissive or non-permissive, may be stated in the
392 | form of a separately written license, or stated as exceptions;
393 | the above requirements apply either way.
394 |
395 | 8. Termination.
396 |
397 | You may not propagate or modify a covered work except as expressly
398 | provided under this License. Any attempt otherwise to propagate or
399 | modify it is void, and will automatically terminate your rights under
400 | this License (including any patent licenses granted under the third
401 | paragraph of section 11).
402 |
403 | However, if you cease all violation of this License, then your
404 | license from a particular copyright holder is reinstated (a)
405 | provisionally, unless and until the copyright holder explicitly and
406 | finally terminates your license, and (b) permanently, if the copyright
407 | holder fails to notify you of the violation by some reasonable means
408 | prior to 60 days after the cessation.
409 |
410 | Moreover, your license from a particular copyright holder is
411 | reinstated permanently if the copyright holder notifies you of the
412 | violation by some reasonable means, this is the first time you have
413 | received notice of violation of this License (for any work) from that
414 | copyright holder, and you cure the violation prior to 30 days after
415 | your receipt of the notice.
416 |
417 | Termination of your rights under this section does not terminate the
418 | licenses of parties who have received copies or rights from you under
419 | this License. If your rights have been terminated and not permanently
420 | reinstated, you do not qualify to receive new licenses for the same
421 | material under section 10.
422 |
423 | 9. Acceptance Not Required for Having Copies.
424 |
425 | You are not required to accept this License in order to receive or
426 | run a copy of the Program. Ancillary propagation of a covered work
427 | occurring solely as a consequence of using peer-to-peer transmission
428 | to receive a copy likewise does not require acceptance. However,
429 | nothing other than this License grants you permission to propagate or
430 | modify any covered work. These actions infringe copyright if you do
431 | not accept this License. Therefore, by modifying or propagating a
432 | covered work, you indicate your acceptance of this License to do so.
433 |
434 | 10. Automatic Licensing of Downstream Recipients.
435 |
436 | Each time you convey a covered work, the recipient automatically
437 | receives a license from the original licensors, to run, modify and
438 | propagate that work, subject to this License. You are not responsible
439 | for enforcing compliance by third parties with this License.
440 |
441 | An "entity transaction" is a transaction transferring control of an
442 | organization, or substantially all assets of one, or subdividing an
443 | organization, or merging organizations. If propagation of a covered
444 | work results from an entity transaction, each party to that
445 | transaction who receives a copy of the work also receives whatever
446 | licenses to the work the party's predecessor in interest had or could
447 | give under the previous paragraph, plus a right to possession of the
448 | Corresponding Source of the work from the predecessor in interest, if
449 | the predecessor has it or can get it with reasonable efforts.
450 |
451 | You may not impose any further restrictions on the exercise of the
452 | rights granted or affirmed under this License. For example, you may
453 | not impose a license fee, royalty, or other charge for exercise of
454 | rights granted under this License, and you may not initiate litigation
455 | (including a cross-claim or counterclaim in a lawsuit) alleging that
456 | any patent claim is infringed by making, using, selling, offering for
457 | sale, or importing the Program or any portion of it.
458 |
459 | 11. Patents.
460 |
461 | A "contributor" is a copyright holder who authorizes use under this
462 | License of the Program or a work on which the Program is based. The
463 | work thus licensed is called the contributor's "contributor version".
464 |
465 | A contributor's "essential patent claims" are all patent claims
466 | owned or controlled by the contributor, whether already acquired or
467 | hereafter acquired, that would be infringed by some manner, permitted
468 | by this License, of making, using, or selling its contributor version,
469 | but do not include claims that would be infringed only as a
470 | consequence of further modification of the contributor version. For
471 | purposes of this definition, "control" includes the right to grant
472 | patent sublicenses in a manner consistent with the requirements of
473 | this License.
474 |
475 | Each contributor grants you a non-exclusive, worldwide, royalty-free
476 | patent license under the contributor's essential patent claims, to
477 | make, use, sell, offer for sale, import and otherwise run, modify and
478 | propagate the contents of its contributor version.
479 |
480 | In the following three paragraphs, a "patent license" is any express
481 | agreement or commitment, however denominated, not to enforce a patent
482 | (such as an express permission to practice a patent or covenant not to
483 | sue for patent infringement). To "grant" such a patent license to a
484 | party means to make such an agreement or commitment not to enforce a
485 | patent against the party.
486 |
487 | If you convey a covered work, knowingly relying on a patent license,
488 | and the Corresponding Source of the work is not available for anyone
489 | to copy, free of charge and under the terms of this License, through a
490 | publicly available network server or other readily accessible means,
491 | then you must either (1) cause the Corresponding Source to be so
492 | available, or (2) arrange to deprive yourself of the benefit of the
493 | patent license for this particular work, or (3) arrange, in a manner
494 | consistent with the requirements of this License, to extend the patent
495 | license to downstream recipients. "Knowingly relying" means you have
496 | actual knowledge that, but for the patent license, your conveying the
497 | covered work in a country, or your recipient's use of the covered work
498 | in a country, would infringe one or more identifiable patents in that
499 | country that you have reason to believe are valid.
500 |
501 | If, pursuant to or in connection with a single transaction or
502 | arrangement, you convey, or propagate by procuring conveyance of, a
503 | covered work, and grant a patent license to some of the parties
504 | receiving the covered work authorizing them to use, propagate, modify
505 | or convey a specific copy of the covered work, then the patent license
506 | you grant is automatically extended to all recipients of the covered
507 | work and works based on it.
508 |
509 | A patent license is "discriminatory" if it does not include within
510 | the scope of its coverage, prohibits the exercise of, or is
511 | conditioned on the non-exercise of one or more of the rights that are
512 | specifically granted under this License. You may not convey a covered
513 | work if you are a party to an arrangement with a third party that is
514 | in the business of distributing software, under which you make payment
515 | to the third party based on the extent of your activity of conveying
516 | the work, and under which the third party grants, to any of the
517 | parties who would receive the covered work from you, a discriminatory
518 | patent license (a) in connection with copies of the covered work
519 | conveyed by you (or copies made from those copies), or (b) primarily
520 | for and in connection with specific products or compilations that
521 | contain the covered work, unless you entered into that arrangement,
522 | or that patent license was granted, prior to 28 March 2007.
523 |
524 | Nothing in this License shall be construed as excluding or limiting
525 | any implied license or other defenses to infringement that may
526 | otherwise be available to you under applicable patent law.
527 |
528 | 12. No Surrender of Others' Freedom.
529 |
530 | If conditions are imposed on you (whether by court order, agreement or
531 | otherwise) that contradict the conditions of this License, they do not
532 | excuse you from the conditions of this License. If you cannot convey a
533 | covered work so as to satisfy simultaneously your obligations under this
534 | License and any other pertinent obligations, then as a consequence you may
535 | not convey it at all. For example, if you agree to terms that obligate you
536 | to collect a royalty for further conveying from those to whom you convey
537 | the Program, the only way you could satisfy both those terms and this
538 | License would be to refrain entirely from conveying the Program.
539 |
540 | 13. Remote Network Interaction; Use with the GNU General Public License.
541 |
542 | Notwithstanding any other provision of this License, if you modify the
543 | Program, your modified version must prominently offer all users
544 | interacting with it remotely through a computer network (if your version
545 | supports such interaction) an opportunity to receive the Corresponding
546 | Source of your version by providing access to the Corresponding Source
547 | from a network server at no charge, through some standard or customary
548 | means of facilitating copying of software. This Corresponding Source
549 | shall include the Corresponding Source for any work covered by version 3
550 | of the GNU General Public License that is incorporated pursuant to the
551 | following paragraph.
552 |
553 | Notwithstanding any other provision of this License, you have
554 | permission to link or combine any covered work with a work licensed
555 | under version 3 of the GNU General Public License into a single
556 | combined work, and to convey the resulting work. The terms of this
557 | License will continue to apply to the part which is the covered work,
558 | but the work with which it is combined will remain governed by version
559 | 3 of the GNU General Public License.
560 |
561 | 14. Revised Versions of this License.
562 |
563 | The Free Software Foundation may publish revised and/or new versions of
564 | the GNU Affero General Public License from time to time. Such new versions
565 | will be similar in spirit to the present version, but may differ in detail to
566 | address new problems or concerns.
567 |
568 | Each version is given a distinguishing version number. If the
569 | Program specifies that a certain numbered version of the GNU Affero General
570 | Public License "or any later version" applies to it, you have the
571 | option of following the terms and conditions either of that numbered
572 | version or of any later version published by the Free Software
573 | Foundation. If the Program does not specify a version number of the
574 | GNU Affero General Public License, you may choose any version ever published
575 | by the Free Software Foundation.
576 |
577 | If the Program specifies that a proxy can decide which future
578 | versions of the GNU Affero General Public License can be used, that proxy's
579 | public statement of acceptance of a version permanently authorizes you
580 | to choose that version for the Program.
581 |
582 | Later license versions may give you additional or different
583 | permissions. However, no additional obligations are imposed on any
584 | author or copyright holder as a result of your choosing to follow a
585 | later version.
586 |
587 | 15. Disclaimer of Warranty.
588 |
589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
597 |
598 | 16. Limitation of Liability.
599 |
600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
608 | SUCH DAMAGES.
609 |
610 | 17. Interpretation of Sections 15 and 16.
611 |
612 | If the disclaimer of warranty and limitation of liability provided
613 | above cannot be given local legal effect according to their terms,
614 | reviewing courts shall apply local law that most closely approximates
615 | an absolute waiver of all civil liability in connection with the
616 | Program, unless a warranty or assumption of liability accompanies a
617 | copy of the Program in return for a fee.
618 |
619 | END OF TERMS AND CONDITIONS
620 |
621 | How to Apply These Terms to Your New Programs
622 |
623 | If you develop a new program, and you want it to be of the greatest
624 | possible use to the public, the best way to achieve this is to make it
625 | free software which everyone can redistribute and change under these terms.
626 |
627 | To do so, attach the following notices to the program. It is safest
628 | to attach them to the start of each source file to most effectively
629 | state the exclusion of warranty; and each file should have at least
630 | the "copyright" line and a pointer to where the full notice is found.
631 |
632 |
633 | Copyright (C)
634 |
635 | This program is free software: you can redistribute it and/or modify
636 | it under the terms of the GNU Affero General Public License as published by
637 | the Free Software Foundation, either version 3 of the License, or
638 | (at your option) any later version.
639 |
640 | This program is distributed in the hope that it will be useful,
641 | but WITHOUT ANY WARRANTY; without even the implied warranty of
642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
643 | GNU Affero General Public License for more details.
644 |
645 | You should have received a copy of the GNU Affero General Public License
646 | along with this program. If not, see .
647 |
648 | Also add information on how to contact you by electronic and paper mail.
649 |
650 | If your software can interact with users remotely through a computer
651 | network, you should also make sure that it provides a way for users to
652 | get its source. For example, if your program is a web application, its
653 | interface could display a "Source" link that leads users to an archive
654 | of the code. There are many ways you could offer source, and different
655 | solutions will be better for different programs; see section 13 for the
656 | specific requirements.
657 |
658 | You should also get your employer (if you work as a programmer) or school,
659 | if any, to sign a "copyright disclaimer" for the program, if necessary.
660 | For more information on this, and how to apply and follow the GNU AGPL, see
661 | .
662 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # FastMirrorServer HTTP API文档
2 |
3 | # 目录
4 |
5 |
6 | | Http request | Description |
7 | |--------------------------------------------------------------------|-------------------|
8 | | [**GET** /api/v3](/#summary) | 获取支持的服务端列表 |
9 | | [**GET** /api/v3/{name}](/#project_info) | 获取服务端部分信息 |
10 | | [**GET** /api/v3/{name}/{mc_version}](/#project_mc_version_info) | 获取获取对应游戏版本的构建版本列表 |
11 | | [**GET** /api/v3/{name}/{mc_version}/{core_version}](/#metadata) | 获取指定核心信息 |
12 | | [**GET** /download/{name}/{mc_version}/{core_version}](/#download) | 下载文件 |
13 |
14 | # 请求返回值
15 | 请求的返回值有统一的格式。
16 |
17 | ```json5
18 | {
19 | "data": [ // 返回的数据
20 | {
21 | "name": "Arclight",
22 | "tag": "mod",
23 | "recommend": true
24 | }
25 | ],
26 | "code": "fin::success", // 请求错误码,请求成功时无意义
27 | // 你也可以通过判断这个码是否为`fin::success`来确定是否成功
28 | "success": true, // 请求是否成功
29 | "message": "Request successfully." // 错误信息,请求成功时无意义。
30 | }
31 | ```
32 | 需要特别注意的是,`.data`的类型不固定,例如部分接口返回的是一个数组,而部分接口返回的是一个对象或别的值。
33 |
34 | 请求失败时,`.data`**一般**都是null。出现内部错误时(http status code == 500),`.data`会携带错误信息,此时将整个请求(包括请求的url、method等)发送给网站管理员,或直接给本项目开issue。
35 |
36 | 以下的文档中,**返回值**一栏均为`.data`中的内容。
37 |
38 | # 请求限额
39 | 普通用户每小时有200次的请求限额;
40 |
41 | 注册用户每小时有500次的请求限额.
42 |
43 | 该限额以后可能会进行调整
44 |
45 | # API
46 |
47 | ## 1. /api/v3
48 |
49 | 获取支持的服务端列表
50 | ### 请求方法
51 | GET
52 | ### 参数
53 | *别问,问就是个patch*
54 |
55 | *参数的返回值参考下一个接口*
56 |
57 | | position | optional | name | type | description |
58 | |----------|----------|---------|-------|------------------|
59 | | query | true | project | array | 查询多个服务端支持的游戏版本列表 |
60 |
61 | ### 返回
62 | **Content-Type**: application/json
63 |
64 | ```json5
65 | [
66 | {
67 | "name": "Arclight", // 核心的名称
68 | "tag": "mod", // 分类,例如mod\pure\proxy等。
69 | "recommend": true // 是否推荐使用
70 | }
71 | /*...*/
72 | ]
73 | ```
74 | ### 身份认证
75 |
76 | 无
77 |
78 | ## 2. /api/v3/{name}
79 |
80 | 获取服务端部分信息
81 | ### 请求方法
82 | GET
83 | ### 参数
84 | | position | optional | name | type | description |
85 | |----------|----------|------|--------|-------------|
86 | | path | false | name | string | 服务端的名称 |
87 |
88 | ### 返回
89 | **Content-Type**: application/json
90 | ```json5
91 | {
92 | "name": "Arclight",
93 | "tag": "mod",
94 | "homepage": "https://github.com/IzzelAliz/Arclight",
95 | "mc_versions": [
96 | "horn",
97 | "GreatHorn",
98 | "1.18",
99 | "1.17",
100 | "1.16",
101 | "1.15"
102 | ]
103 | }
104 | ```
105 | ### 身份认证
106 |
107 | 无
108 |
109 | ## 3. /api/v3/{name}/{mc_version}
110 |
111 | 获取获取对应游戏版本的构建版本列表
112 | ### 请求方法
113 | GET
114 | ### 参数
115 | | position | optional | name | type | description |
116 | |----------|----------|------------|--------|-------------|
117 | | path | false | name | string | 服务端的名称 |
118 | | path | false | mc_version | string | mc版本 |
119 | | query | true | offset | int | 从第几个开始查询 |
120 | | query | true | count | int | 查询的数量 |
121 |
122 | ### 返回
123 | **Content-Type**: application/json
124 | ```json5
125 | {
126 | "builds": [{
127 | "name": "Arclight",
128 | "mc_version": "1.18", // mc版本
129 | "core_version": "1.0.8", // 核心文件版本
130 | "update_time": "2023-02-02T07:50:37", // 更新时间,UTC
131 | "sha1": "6922a497d8d3204345d5fe83c04bbec4a6f456b6" // 文件校验值
132 | }],
133 | "offset": 0, // 从第几个开始查询
134 | "limit": 1, // 查询的数量
135 | "count": 9 // 总个数
136 | }
137 | ```
138 | ### 身份认证
139 |
140 | 无
141 |
142 | ## 4. /api/v3/{name}/{mc_version}/{core_version}
143 |
144 | 获取指定核心信息
145 | ### 请求方法
146 | GET
147 | ### 参数
148 | | position | optional | name | type | description |
149 | |----------|----------|--------------|--------|-------------|
150 | | path | false | name | string | 服务端的名称 |
151 | | path | false | mc_version | string | mc版本 |
152 | | path | false | core_version | string | 构建版本 |
153 |
154 | ### 返回
155 | **Content-Type**: application/json
156 | ```json5
157 | {
158 | "name": "Arclight",
159 | "mc_version": "1.18",
160 | "core_version": "1.0.8",
161 | "update_time": "2023-02-02T07:50:37",
162 | "sha1": "6922a497d8d3204345d5fe83c04bbec4a6f456b6",
163 | "filename": "Arclight-1.18-1.0.8.jar",
164 | "download_url": "http://localhost/download/Arclight/1.18/1.0.8"
165 | }
166 | ```
167 | ### 身份认证
168 |
169 | 无
170 |
171 | ## 5. /download/{name}/{mc_version}/{core_version}
172 |
173 | 下载文件
174 |
175 | 建议使用 [/api/v3/{name}/{mc_version}/{core_version}](/#metadata)来获取下载地址,而不是手动拼接url。
176 |
177 | ### 请求方法
178 | GET
179 | ### 参数
180 | | position | optional | name | type | description |
181 | |----------|----------|--------------|--------|-------------|
182 | | path | false | name | string | 服务端的名称 |
183 | | path | false | mc_version | string | mc版本 |
184 | | path | false | core_version | string | 构建版本 |
185 |
186 | ### 返回
187 | **Content-Type**: application/octet-stream
188 |
189 | ### 身份认证
190 |
191 | 无
192 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
2 | import java.io.ByteArrayOutputStream
3 |
4 | plugins {
5 | kotlin("jvm") version "1.8.10"
6 | id ("org.jetbrains.kotlin.plugin.spring") version "1.8.10"
7 | id ("org.springframework.boot") version "2.6.3"
8 | id ("io.spring.dependency-management") version "1.0.11.RELEASE"
9 | }
10 |
11 | group = "com.github.fastmirrorserver"
12 | version = "0.0.0"
13 |
14 | repositories {
15 | mavenCentral()
16 | }
17 |
18 | dependencies {
19 | implementation("org.jetbrains.kotlin:kotlin-stdlib:1.8.10")
20 |
21 | implementation("org.postgresql:postgresql:42.3.8")
22 |
23 | implementation("org.springframework.boot:spring-boot-starter-jdbc:2.7.2")
24 | implementation("org.springframework.boot:spring-boot-starter:2.7.2")
25 | implementation("org.springframework.boot:spring-boot-starter-web:2.7.2")
26 | implementation("org.springframework.boot:spring-boot-starter-data-redis:2.7.2")
27 |
28 | implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.3")
29 |
30 | implementation("org.ktorm:ktorm-core:3.5.0")
31 | implementation("org.ktorm:ktorm-support-postgresql:3.5.0")
32 | implementation("org.ktorm:ktorm-jackson:3.5.0")
33 |
34 | testImplementation(kotlin("test"))
35 | testImplementation("org.springframework.boot:spring-boot-starter-test:2.7.0")
36 |
37 | annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
38 | }
39 |
40 | tasks.test {
41 | useJUnitPlatform()
42 | }
43 |
44 | tasks.withType {
45 | kotlinOptions.jvmTarget = "11"
46 | }
47 | fun getTag(): String {
48 | val stdout = ByteArrayOutputStream()
49 | exec {
50 | commandLine("git", "describe", "--tags")
51 | standardOutput = stdout
52 | }
53 | val output = stdout.toString("UTF-8")
54 | return (if(output[0] == 'v') output.substring(1)
55 | else output).trim()
56 | }
57 | fun getCommitId(): String {
58 | val stdout = ByteArrayOutputStream()
59 | exec {
60 | commandLine("git", "rev-parse", "--short", "HEAD")
61 | standardOutput = stdout
62 | }
63 | return stdout.toString("UTF-8").trim()
64 | }
65 | tasks.bootJar {
66 | val ver = "${getTag()}-${getCommitId()}"
67 | println(ver)
68 | project.version = ver
69 | archiveFileName.set("server.jar")
70 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | kotlin.code.style=official
2 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FastMirror-MC/FastMirrorServer/2dc28552c54d24fc99b63705d56426861ffdb440/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | #
21 | # Gradle start up script for POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
84 |
85 | APP_NAME="Gradle"
86 | APP_BASE_NAME=${0##*/}
87 |
88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
90 |
91 | # Use the maximum available, or set MAX_FD != -1 to use that value.
92 | MAX_FD=maximum
93 |
94 | warn () {
95 | echo "$*"
96 | } >&2
97 |
98 | die () {
99 | echo
100 | echo "$*"
101 | echo
102 | exit 1
103 | } >&2
104 |
105 | # OS specific support (must be 'true' or 'false').
106 | cygwin=false
107 | msys=false
108 | darwin=false
109 | nonstop=false
110 | case "$( uname )" in #(
111 | CYGWIN* ) cygwin=true ;; #(
112 | Darwin* ) darwin=true ;; #(
113 | MSYS* | MINGW* ) msys=true ;; #(
114 | NONSTOP* ) nonstop=true ;;
115 | esac
116 |
117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
118 |
119 |
120 | # Determine the Java command to use to start the JVM.
121 | if [ -n "$JAVA_HOME" ] ; then
122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
123 | # IBM's JDK on AIX uses strange locations for the executables
124 | JAVACMD=$JAVA_HOME/jre/sh/java
125 | else
126 | JAVACMD=$JAVA_HOME/bin/java
127 | fi
128 | if [ ! -x "$JAVACMD" ] ; then
129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
130 |
131 | Please set the JAVA_HOME variable in your environment to match the
132 | location of your Java installation."
133 | fi
134 | else
135 | JAVACMD=java
136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
137 |
138 | Please set the JAVA_HOME variable in your environment to match the
139 | location of your Java installation."
140 | fi
141 |
142 | # Increase the maximum file descriptors if we can.
143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
144 | case $MAX_FD in #(
145 | max*)
146 | MAX_FD=$( ulimit -H -n ) ||
147 | warn "Could not query maximum file descriptor limit"
148 | esac
149 | case $MAX_FD in #(
150 | '' | soft) :;; #(
151 | *)
152 | ulimit -n "$MAX_FD" ||
153 | warn "Could not set maximum file descriptor limit to $MAX_FD"
154 | esac
155 | fi
156 |
157 | # Collect all arguments for the java command, stacking in reverse order:
158 | # * args from the command line
159 | # * the main class name
160 | # * -classpath
161 | # * -D...appname settings
162 | # * --module-path (only if needed)
163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
164 |
165 | # For Cygwin or MSYS, switch paths to Windows format before running java
166 | if "$cygwin" || "$msys" ; then
167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
169 |
170 | JAVACMD=$( cygpath --unix "$JAVACMD" )
171 |
172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
173 | for arg do
174 | if
175 | case $arg in #(
176 | -*) false ;; # don't mess with options #(
177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
178 | [ -e "$t" ] ;; #(
179 | *) false ;;
180 | esac
181 | then
182 | arg=$( cygpath --path --ignore --mixed "$arg" )
183 | fi
184 | # Roll the args list around exactly as many times as the number of
185 | # args, so each arg winds up back in the position where it started, but
186 | # possibly modified.
187 | #
188 | # NB: a `for` loop captures its iteration list before it begins, so
189 | # changing the positional parameters here affects neither the number of
190 | # iterations, nor the values presented in `arg`.
191 | shift # remove old arg
192 | set -- "$@" "$arg" # push replacement arg
193 | done
194 | fi
195 |
196 | # Collect all arguments for the java command;
197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
198 | # shell script including quotes and variable substitutions, so put them in
199 | # double quotes to make sure that they get re-expanded; and
200 | # * put everything else in single quotes, so that it's not re-expanded.
201 |
202 | set -- \
203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
204 | -classpath "$CLASSPATH" \
205 | org.gradle.wrapper.GradleWrapperMain \
206 | "$@"
207 |
208 | # Use "xargs" to parse quoted args.
209 | #
210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
211 | #
212 | # In Bash we could simply go:
213 | #
214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
215 | # set -- "${ARGS[@]}" "$@"
216 | #
217 | # but POSIX shell has neither arrays nor command substitution, so instead we
218 | # post-process each arg (as a line of input to sed) to backslash-escape any
219 | # character that might be a shell metacharacter, then use eval to reverse
220 | # that process (while maintaining the separation between arguments), and wrap
221 | # the whole thing up as a single "set" statement.
222 | #
223 | # This will of course break if any of these variables contains a newline or
224 | # an unmatched quote.
225 | #
226 |
227 | eval "set -- $(
228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
229 | xargs -n1 |
230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
231 | tr '\n' ' '
232 | )" '"$@"'
233 |
234 | exec "$JAVACMD" "$@"
235 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 |
2 | rootProject.name = "FastmirrorServer"
3 |
4 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/fastmirrorserver/Application.kt:
--------------------------------------------------------------------------------
1 | package com.github.fastmirrorserver
2 |
3 | import org.springframework.boot.SpringApplication
4 | import org.springframework.boot.autoconfigure.SpringBootApplication
5 |
6 | @SpringBootApplication
7 | class Application
8 |
9 | fun main(args: Array) {
10 | SpringApplication.run(Application::class.java, *args)
11 | }
12 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/fastmirrorserver/annotation/Authority.kt:
--------------------------------------------------------------------------------
1 | package com.github.fastmirrorserver.annotation
2 |
3 | import com.github.fastmirrorserver.util.enums.Permission
4 |
5 | annotation class Authority(val permission: Permission = Permission.NONE, val ignore_in_debug:Boolean = true)
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/fastmirrorserver/annotation/RawResponse.kt:
--------------------------------------------------------------------------------
1 | package com.github.fastmirrorserver.annotation
2 |
3 | annotation class RawResponse()
4 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/fastmirrorserver/component/Cache.kt:
--------------------------------------------------------------------------------
1 | package com.github.fastmirrorserver.component
2 |
3 | import java.time.LocalDateTime
4 |
5 | interface Cache {
6 | fun add(key: String, value: T): Boolean
7 | fun add(key: String, value: T, expire: Long): Boolean
8 | fun add(key: String, value: T, expire: LocalDateTime): Boolean
9 | fun upd(key: String, value: T)
10 | fun upd(key: String, value: T, expire: Long)
11 | fun upd(key: String, value: T, expire: LocalDateTime)
12 | fun get(key: String, type: Class): T?
13 | fun pop(key: String, type: Class): T?
14 | fun has(key: String): Boolean
15 | fun del(key: String): Boolean
16 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/fastmirrorserver/component/MemoryCache.kt:
--------------------------------------------------------------------------------
1 | package com.github.fastmirrorserver.component
2 |
3 | import com.github.fastmirrorserver.util.Json
4 | import com.github.fastmirrorserver.util.timestamp
5 | import org.springframework.stereotype.Component
6 | import java.time.Duration
7 | import java.time.LocalDateTime
8 | import java.util.TreeMap
9 |
10 | @Component
11 | class MemoryCache : Cache {
12 | private class Node(value: T, val expire: Long = Int.MAX_VALUE + timestamp()) {
13 | private val _value = Json.serialization(value)
14 | fun convert(type: Class): T = Json.deserialization(_value, type)
15 | }
16 | private val cache = TreeMap>();
17 |
18 | override fun add(key: String, value: T): Boolean {
19 | if(has(key)) return false
20 | cache[key] = Node(value)
21 | return true
22 | }
23 |
24 | override fun add(key: String, value: T, expire: Long): Boolean {
25 | if(has(key)) return false
26 | cache[key] = Node(value, expire)
27 | return true
28 | }
29 |
30 | override fun add(key: String, value: T, expire: LocalDateTime): Boolean {
31 | add(key, value, Duration.between(LocalDateTime.now(), expire).seconds)
32 | return false
33 | }
34 |
35 | override fun upd(key: String, value: T) { cache[key] = Node(value) }
36 | override fun upd(key: String, value: T, expire: Long) { cache[key] = Node(value, expire) }
37 | override fun upd(key: String, value: T, expire: LocalDateTime) { cache[key] = Node(value, Duration.between(LocalDateTime.now(), expire).seconds) }
38 |
39 | override fun get(key: String, type: Class): T? = cache[key]?.convert(type)
40 | override fun pop(key: String, type: Class): T? = cache.remove(key)?.convert(type)
41 |
42 | override fun has(key: String): Boolean {
43 | val result = cache[key]?.let { it.expire > timestamp() } ?: return false
44 | if(result) return true
45 | del(key)
46 | return false
47 | }
48 |
49 | override fun del(key: String) = cache.remove(key) != null
50 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/fastmirrorserver/config/DataSourceInitializer.kt:
--------------------------------------------------------------------------------
1 | package com.github.fastmirrorserver.config
2 |
3 | import org.springframework.beans.factory.annotation.Value
4 | import org.springframework.context.annotation.Bean
5 | import org.springframework.context.annotation.Configuration
6 | import org.springframework.core.io.Resource
7 | import org.springframework.jdbc.datasource.init.DataSourceInitializer
8 | import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator
9 | import javax.sql.DataSource
10 |
11 | @Configuration
12 | class DataSourceInitializer {
13 | @Value("classpath:table.sql")
14 | private lateinit var _script_create_table: Resource
15 |
16 | @Bean
17 | fun initializer(source: DataSource) = with(DataSourceInitializer()) {
18 | this.setDataSource(source)
19 | this.setDatabasePopulator(databasePopulator())
20 | return@with this
21 | }
22 |
23 | private fun databasePopulator() = with(ResourceDatabasePopulator()) {
24 | this.addScript(_script_create_table)
25 | return@with this
26 | }
27 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/fastmirrorserver/config/InterceptorConfiguration.kt:
--------------------------------------------------------------------------------
1 | package com.github.fastmirrorserver.config
2 |
3 | import com.github.fastmirrorserver.interceptor.AuthorizationInterceptor
4 | import com.github.fastmirrorserver.interceptor.ResponseResultInterceptor
5 | import org.springframework.beans.factory.annotation.Autowired
6 | import org.springframework.context.annotation.Configuration
7 | import org.springframework.web.servlet.config.annotation.InterceptorRegistry
8 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
9 |
10 | @Configuration
11 | class InterceptorConfiguration : WebMvcConfigurer {
12 | @Autowired
13 | private lateinit var authorization_interceptor: AuthorizationInterceptor
14 | @Autowired
15 | private lateinit var response_result_interceptor: ResponseResultInterceptor
16 |
17 | override fun addInterceptors(registry: InterceptorRegistry) {
18 | registry.addInterceptor(authorization_interceptor)
19 | .addPathPatterns("/**")
20 | .excludePathPatterns("/static/**")
21 | .excludePathPatterns("/error")
22 | registry.addInterceptor(response_result_interceptor)
23 | .addPathPatterns("/**")
24 | .excludePathPatterns("/static/**")
25 | .excludePathPatterns("/error")
26 | }
27 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/fastmirrorserver/config/KtOrmConfiguration.kt:
--------------------------------------------------------------------------------
1 | package com.github.fastmirrorserver.config
2 |
3 | import org.ktorm.database.Database
4 | import org.ktorm.jackson.KtormModule
5 | import org.ktorm.support.postgresql.PostgreSqlDialect
6 | import org.springframework.beans.factory.annotation.Value
7 | import org.springframework.context.annotation.Bean
8 | import org.springframework.context.annotation.Configuration
9 |
10 | @Configuration
11 | class KtOrmConfiguration {
12 | @Value("\${spring.datasource.url}")
13 | private lateinit var url: String
14 | @Value("\${spring.datasource.username}")
15 | private lateinit var user: String
16 | @Value("\${spring.datasource.password}")
17 | private lateinit var password: String
18 | @Value("\${spring.datasource.driver-class-name}")
19 | private lateinit var driver: String
20 |
21 | @Bean
22 | fun database(): Database = Database.connect(url, driver, user, password, dialect = PostgreSqlDialect())
23 |
24 | @Bean
25 | fun ktOrmModule(): KtormModule = KtormModule()
26 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/fastmirrorserver/controller/AccountController.kt:
--------------------------------------------------------------------------------
1 | package com.github.fastmirrorserver.controller
2 |
3 | import com.github.fastmirrorserver.annotation.Authority
4 | import com.github.fastmirrorserver.pojo.AccountPOJO
5 | import com.github.fastmirrorserver.service.AuthorizationService
6 | import com.github.fastmirrorserver.util.enums.Permission
7 | import org.springframework.beans.factory.annotation.Autowired
8 | import org.springframework.http.HttpStatus
9 | import org.springframework.web.bind.annotation.PostMapping
10 | import org.springframework.web.bind.annotation.RequestBody
11 | import org.springframework.web.bind.annotation.ResponseStatus
12 | import org.springframework.web.bind.annotation.RestController
13 |
14 | @RestController
15 | class AccountController {
16 | companion object {
17 | const val REGISTER = "/api/v3/auth/register"
18 | }
19 | @Autowired
20 | private lateinit var authorization: AuthorizationService
21 | @PostMapping(REGISTER, consumes= ["application/json"])
22 | @ResponseStatus(value = HttpStatus.NO_CONTENT)
23 | @Authority(Permission.ROOT)
24 | fun register(@RequestBody pojo: AccountPOJO) = authorization.registerOrUpdate(pojo)
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/fastmirrorserver/controller/ApiExceptionHandler.kt:
--------------------------------------------------------------------------------
1 | package com.github.fastmirrorserver.controller
2 |
3 | import com.github.fastmirrorserver.dto.ApiResponse
4 | import com.github.fastmirrorserver.exception.ApiException
5 | import com.github.fastmirrorserver.service.ErrorReportService
6 | import com.github.fastmirrorserver.util.UTC
7 | import com.github.fastmirrorserver.util.uuid
8 | import org.slf4j.LoggerFactory
9 | import org.springframework.beans.factory.annotation.Autowired
10 | import org.springframework.web.bind.annotation.ExceptionHandler
11 | import org.springframework.web.bind.annotation.RestControllerAdvice
12 | import org.springframework.web.util.NestedServletException
13 | import java.io.File
14 | import java.io.PrintWriter
15 | import java.lang.Exception
16 | import java.time.LocalDateTime
17 | import javax.servlet.http.HttpServletRequest
18 | import javax.servlet.http.HttpServletResponse
19 |
20 | @RestControllerAdvice
21 | class ApiExceptionHandler {
22 | private val logger = LoggerFactory.getLogger(this::class.java)
23 | @Autowired
24 | private lateinit var service: ErrorReportService
25 |
26 | @ExceptionHandler(ApiException::class)
27 | fun serviceExceptionHandler(e: ApiException, request: HttpServletRequest, response: HttpServletResponse): ApiResponse {
28 | response.status = e.status.value()
29 | return e.toResponse()
30 | }
31 |
32 | @ExceptionHandler(Exception::class)
33 | fun exceptionHandler(e: Exception, request: HttpServletRequest, response: HttpServletResponse): ApiResponse {
34 | if(e.cause != null && e.cause is ApiException) return serviceExceptionHandler(e.cause as ApiException, request, response)
35 | logger.error("uncaught exception: ", e)
36 | response.status = 500
37 |
38 | val id = service.set(e)
39 |
40 | return ApiResponse(
41 | data = mapOf(
42 | "name" to e::class.java.name,
43 | "message" to (e.message ?: "none"),
44 | "x-ray-id" to id
45 | ),
46 | code = "err::internal_server_error",
47 | success = false,
48 | message = "Internal server error. Please send this message to website administrator."
49 | )
50 | }
51 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/fastmirrorserver/controller/ApiResponseHandler.kt:
--------------------------------------------------------------------------------
1 | package com.github.fastmirrorserver.controller
2 |
3 | import com.github.fastmirrorserver.annotation.RawResponse
4 | import com.github.fastmirrorserver.dto.ApiResponse
5 | import org.springframework.core.MethodParameter
6 | import org.springframework.http.HttpStatus
7 | import org.springframework.http.MediaType
8 | import org.springframework.http.converter.HttpMessageConverter
9 | import org.springframework.http.server.ServerHttpRequest
10 | import org.springframework.http.server.ServerHttpResponse
11 | import org.springframework.web.bind.annotation.ControllerAdvice
12 | import org.springframework.web.bind.annotation.ResponseStatus
13 | import org.springframework.web.context.request.RequestContextHolder
14 | import org.springframework.web.context.request.ServletRequestAttributes
15 | import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice
16 | import java.lang.Exception
17 |
18 | @ControllerAdvice
19 | class ApiResponseHandler : ResponseBodyAdvice {
20 | override fun supports(returnType: MethodParameter, converterType: Class>): Boolean {
21 | val attributes = RequestContextHolder.getRequestAttributes() as ServletRequestAttributes
22 | return attributes.request.getAttribute("DISABLE_RESPONSE_WRAPPER") == null
23 | }
24 |
25 | override fun beforeBodyWrite(
26 | body: Any?,
27 | returnType: MethodParameter,
28 | selectedContentType: MediaType,
29 | selectedConverterType: Class>,
30 | request: ServerHttpRequest,
31 | response: ServerHttpResponse
32 | ): Any? {
33 | val no_content = returnType.getMethodAnnotation(ResponseStatus::class.java)?.let {
34 | it.code == HttpStatus.NO_CONTENT || it.value == HttpStatus.NO_CONTENT
35 | } ?: false
36 | val raw_response = returnType.getMethodAnnotation(RawResponse::class.java) != null
37 |
38 | if(no_content || raw_response || body is String || body is ApiResponse) return body
39 | if(body is Exception) return ApiResponse.failed(body)
40 | return ApiResponse.success(body)
41 | }
42 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/fastmirrorserver/controller/HttpErrorController.kt:
--------------------------------------------------------------------------------
1 | package com.github.fastmirrorserver.controller
2 |
3 | import com.github.fastmirrorserver.dto.ApiResponse
4 | import org.springframework.boot.web.servlet.error.ErrorController
5 | import org.springframework.http.HttpStatus
6 | import org.springframework.web.bind.annotation.RequestMapping
7 | import org.springframework.web.bind.annotation.RestController
8 | import javax.servlet.http.HttpServletRequest
9 |
10 | @RestController
11 | class HttpErrorController : ErrorController {
12 | @RequestMapping("/error")
13 | fun handler(request: HttpServletRequest): ApiResponse {
14 | val code = request.getAttribute("javax.servlet.error.status_code") as Int
15 | val status = HttpStatus.valueOf(code)
16 | return ApiResponse(
17 | data = null,
18 | code = "err::status_code::${status.series().name.lowercase()}",
19 | success = false,
20 | message = status.reasonPhrase
21 | )
22 | }
23 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/fastmirrorserver/controller/QueryController.kt:
--------------------------------------------------------------------------------
1 | package com.github.fastmirrorserver.controller
2 |
3 | import com.github.fastmirrorserver.annotation.RawResponse
4 | import com.github.fastmirrorserver.entity.toResponse
5 | import com.github.fastmirrorserver.service.FileService
6 | import com.github.fastmirrorserver.util.*
7 | import org.ktorm.database.Database
8 | import org.springframework.beans.factory.annotation.Autowired
9 | import org.springframework.web.bind.annotation.GetMapping
10 | import org.springframework.web.bind.annotation.PathVariable
11 | import org.springframework.web.bind.annotation.RequestParam
12 | import org.springframework.web.bind.annotation.RestController
13 | import javax.servlet.http.HttpServletRequest
14 |
15 | @RestController
16 | class QueryController {
17 | companion object {
18 | const val QUERY_ALL_PROJECT = "/api/v3"
19 | const val QUERY_SUPPORTED_MC_VER_OF_PROJECT = "/api/v3/{name}"
20 | const val QUERY_ALL_CORE_VER_OF_MC_VER = "/api/v3/{name}/{mc_version}"
21 | const val QUERY_SPECIFIC_ARTIFACT = "/api/v3/{name}/{mc_version}/{core_version}"
22 | const val DOWNLOAD = "/download/{name}/{mc_version}/{core_version}"
23 | }
24 |
25 | @Autowired
26 | private lateinit var database: Database
27 | @Autowired
28 | private lateinit var file_service: FileService
29 |
30 | @GetMapping(QUERY_ALL_PROJECT)
31 | fun queryAllProject(@RequestParam("name", required = false) projects: ArrayList?)
32 | = projects?.let { database.getSupportedMcVersionOfProjects(it) } ?: database.getAllProjects()
33 |
34 | @GetMapping(QUERY_SUPPORTED_MC_VER_OF_PROJECT)
35 | fun querySupportedMcVersionOfProject(
36 | @PathVariable name: String
37 | ) = database.getSupportedMcVersionOfProject(name)
38 |
39 | @GetMapping(QUERY_ALL_CORE_VER_OF_MC_VER)
40 | fun queryAllCoreVersionOfMcVersion(
41 | @PathVariable name: String,
42 | @PathVariable mc_version: String,
43 | @RequestParam("offset", required = false) offset: Int?,
44 | @RequestParam("limit", required = false) limit: Int?
45 | ) = database.getAllCoreVersionOfMcVersion(name, mc_version, offset, limit)
46 |
47 | @GetMapping(QUERY_SPECIFIC_ARTIFACT)
48 | fun querySpecificArtifact(
49 | @PathVariable name: String,
50 | @PathVariable core_version: String,
51 | @PathVariable mc_version: String,
52 | request: HttpServletRequest
53 | ) = database.getSpecificArtifact(name, mc_version, core_version)
54 | .toResponse(request)
55 |
56 | @GetMapping(DOWNLOAD)
57 | @RawResponse
58 | fun download(
59 | @PathVariable name: String,
60 | @PathVariable core_version: String,
61 | @PathVariable mc_version: String
62 | ) = file_service.send(name, core_version, mc_version)
63 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/fastmirrorserver/controller/TestController.kt:
--------------------------------------------------------------------------------
1 | package com.github.fastmirrorserver.controller
2 |
3 | import com.github.fastmirrorserver.annotation.Authority
4 | import com.github.fastmirrorserver.util.enums.Permission
5 | import com.github.fastmirrorserver.util.signature
6 | import org.slf4j.LoggerFactory
7 | import org.springframework.web.bind.annotation.PutMapping
8 | import org.springframework.web.bind.annotation.RequestMapping
9 | import org.springframework.web.bind.annotation.RestController
10 | import javax.servlet.http.HttpServletRequest
11 |
12 | @RestController
13 | class TestController {
14 | private val logger = LoggerFactory.getLogger(this::class.java)
15 | companion object {
16 | const val DEV_NULL = "/test/dev/null"
17 | const val PERMISSION_TEST = "/test/permission"
18 | const val UPLOAD_TEST = "/test/upload"
19 | }
20 | @RequestMapping(DEV_NULL, )
21 | @Authority(Permission.TESTER)
22 | fun `dev null`() {}
23 |
24 | @RequestMapping(PERMISSION_TEST)
25 | @Authority(Permission.ROOT, ignore_in_debug = false)
26 | fun `permission test`() {}
27 |
28 | @PutMapping(UPLOAD_TEST)
29 | fun `bytes upload test`(request: HttpServletRequest): String {
30 | val data = request.inputStream.readAllBytes() ?: ByteArray(0)
31 | logger.info("Range: ${request.getHeader("Range") ?: "null"}")
32 | logger.info("Content-Length: ${request.getHeader("Content-Length") ?: "null"}")
33 | logger.info("body_size = ${data.size}")
34 | logger.info("sha1 = ${data.signature()}")
35 | return data.signature()
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/fastmirrorserver/controller/TracebackController.kt:
--------------------------------------------------------------------------------
1 | package com.github.fastmirrorserver.controller
2 |
3 | import com.github.fastmirrorserver.annotation.Authority
4 | import com.github.fastmirrorserver.service.ErrorReportService
5 | import com.github.fastmirrorserver.util.enums.Permission
6 | import org.springframework.beans.factory.annotation.Autowired
7 | import org.springframework.stereotype.Controller
8 | import org.springframework.web.bind.annotation.GetMapping
9 | import org.springframework.web.bind.annotation.PathVariable
10 |
11 | @Controller
12 | class TracebackController {
13 | @Autowired
14 | private lateinit var service: ErrorReportService
15 |
16 | @GetMapping("/monitor/error-report/{id}")
17 | @Authority(Permission.ROOT)
18 | fun display(@PathVariable id: String): String = service.get(id)
19 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/fastmirrorserver/controller/UploadTaskController.kt:
--------------------------------------------------------------------------------
1 | package com.github.fastmirrorserver.controller
2 |
3 | import com.github.fastmirrorserver.annotation.Authority
4 | import com.github.fastmirrorserver.pojo.ManifestPOJO
5 | import com.github.fastmirrorserver.service.UploadTaskService
6 | import com.github.fastmirrorserver.util.enums.Permission
7 | import org.springframework.beans.factory.annotation.Autowired
8 | import org.springframework.web.bind.annotation.PathVariable
9 | import org.springframework.web.bind.annotation.PostMapping
10 | import org.springframework.web.bind.annotation.PutMapping
11 | import org.springframework.web.bind.annotation.RequestBody
12 | import org.springframework.web.bind.annotation.RestController
13 | import javax.servlet.http.HttpServletRequest
14 |
15 | @RestController
16 | class UploadTaskController {
17 | @Autowired
18 | private lateinit var service: UploadTaskService
19 | companion object {
20 | const val CREATE_TASK = "/api/v3/upload/session/create"
21 | const val UPLOAD = "/api/v3/upload/session/file/{name}/{mc_version}/{core_version}"
22 | const val CLOSE_TASK = "/api/v3/upload/session/close/{name}/{mc_version}/{core_version}"
23 | }
24 |
25 | @PostMapping(CREATE_TASK)
26 | @Authority(Permission.COLLECTOR)
27 | fun createTask(@RequestBody manifest: ManifestPOJO, request: HttpServletRequest)
28 | = service.createTask(manifest, request)
29 |
30 | @PutMapping(UPLOAD)
31 | @Authority(Permission.COLLECTOR)
32 | fun upload(
33 | @PathVariable name: String,
34 | @PathVariable mc_version: String,
35 | @PathVariable core_version: String,
36 | request: HttpServletRequest
37 | ) = service.uploadFile(name, mc_version, core_version, request)
38 |
39 | @PutMapping(CLOSE_TASK)
40 | @Authority(Permission.COLLECTOR)
41 | fun closeTask(
42 | @PathVariable name: String,
43 | @PathVariable mc_version: String,
44 | @PathVariable core_version: String
45 | ) = service.closeTask(name, mc_version, core_version)
46 | }
47 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/fastmirrorserver/dto/ApiResponse.kt:
--------------------------------------------------------------------------------
1 | package com.github.fastmirrorserver.dto
2 |
3 | data class ApiResponse(
4 | val data: Any?,
5 | val code: String,
6 | val success: Boolean,
7 | val message: String
8 | ) {
9 | companion object {
10 | fun success(data: Any? = null) = ApiResponse(
11 | data = data,
12 | code = "fin::success",
13 | success = true,
14 | message = "Request successfully."
15 | )
16 | fun failed(data: Any? = null) = ApiResponse(
17 | data = data,
18 | code = "err::unknown",
19 | success = false,
20 | message = "Unknown error."
21 | )
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/fastmirrorserver/dto/Metadata.kt:
--------------------------------------------------------------------------------
1 | package com.github.fastmirrorserver.dto
2 |
3 | import com.fasterxml.jackson.annotation.JsonIgnore
4 | import com.github.fastmirrorserver.controller.UploadTaskController
5 | import com.github.fastmirrorserver.util.assemblyURL
6 | import javax.servlet.http.HttpServletRequest
7 |
8 | open class Metadata(
9 | val name: String,
10 | val mc_version: String,
11 | val core_version: String
12 | ) {
13 | @get:JsonIgnore
14 | val key: String get() = "$name-$mc_version-$core_version"
15 |
16 | fun uploadUrl(request: HttpServletRequest): String
17 | = request.assemblyURL(UploadTaskController.UPLOAD, mapOf(
18 | "name" to name,
19 | "mc_version" to mc_version,
20 | "core_version" to core_version
21 | ))
22 | }
23 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/fastmirrorserver/dto/Summary.kt:
--------------------------------------------------------------------------------
1 | package com.github.fastmirrorserver.dto
2 |
3 | data class Summary(
4 | val name: String,
5 | val tag: String,
6 | val recommend: Boolean
7 | )
8 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/fastmirrorserver/dto/Traceback.kt:
--------------------------------------------------------------------------------
1 | package com.github.fastmirrorserver.dto
2 |
3 | import com.fasterxml.jackson.annotation.JsonIgnore
4 | import com.github.fastmirrorserver.util.enums.Permission
5 | import com.github.fastmirrorserver.util.uuid
6 | import java.time.LocalDateTime
7 | import javax.servlet.http.Cookie
8 |
9 | data class Traceback(
10 | val user: String,
11 | val remote_address: String,
12 | val permission: Permission,
13 | val token: String = uuid(),
14 | private var _remain_request_count: Int = permission.request_limit,
15 | private var _next_refresh_time: LocalDateTime = LocalDateTime.now().plusSeconds(permission.refresh_period.toLong()),
16 | val lifetime: LocalDateTime = LocalDateTime.now().plusHours(2)
17 | ) {
18 | companion object {
19 | const val COOKIE_NAME = "traceback"
20 | }
21 | @get:JsonIgnore
22 | val remain_request_count get() = if(_remain_request_count < 0) 0 else _remain_request_count
23 | @get:JsonIgnore
24 | val next_refresh_time get() = _next_refresh_time
25 |
26 | fun requestable(): Boolean {
27 | if(permission.unlimited_request) return true
28 | if(_next_refresh_time < LocalDateTime.now()) {
29 | _remain_request_count = permission.request_limit
30 | _next_refresh_time = LocalDateTime.now().plusSeconds(permission.refresh_period.toLong())
31 | }
32 |
33 | // 你能一分钟访问2,147,483,648次我把键盘吃下去
34 | // return if(_remain_request_count == 0) false else _remain_request_count --> 0
35 | return _remain_request_count --> 0
36 | }
37 |
38 | fun toCookie(): Cookie {
39 | val cookie = Cookie(COOKIE_NAME, token)
40 | cookie.path = "/"
41 | cookie.maxAge = -1
42 | cookie.secure = true
43 | return cookie
44 | }
45 |
46 | override fun toString(): String
47 | = "Session{user=$user, token=$token, ip=$remote_address, remain_request=$remain_request_count, next_refresh_time=$next_refresh_time}"
48 | }
49 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/fastmirrorserver/entity/Account.kt:
--------------------------------------------------------------------------------
1 | package com.github.fastmirrorserver.entity
2 |
3 | import com.github.fastmirrorserver.util.enums.Permission
4 | import com.github.fastmirrorserver.util.signature
5 | import org.ktorm.entity.Entity
6 | import org.ktorm.schema.Table
7 | import org.ktorm.schema.datetime
8 | import org.ktorm.schema.enum
9 | import org.ktorm.schema.varchar
10 | import java.time.LocalDateTime
11 |
12 | interface Account : Entity {
13 | companion object: Entity.Factory()
14 | var name: String
15 | var authorization_string: String
16 | var permission: Permission
17 | var last_login: LocalDateTime
18 |
19 | fun verify(password: String): Boolean {
20 | val (method, digest, salt) = authorization_string.substring(1).split(":")
21 | return digest == "$password:$salt".signature(method)
22 | }
23 | }
24 |
25 | object Accounts : Table("t_account") {
26 | val name = varchar("name").primaryKey().bindTo { it.name }
27 | val authorization_string = varchar("authorization_string").bindTo { it.authorization_string }
28 | val permission = enum("permission").bindTo { it.permission }
29 | val last_login = datetime("last_login").bindTo { it.last_login }
30 | }
31 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/fastmirrorserver/entity/Manifest.kt:
--------------------------------------------------------------------------------
1 | package com.github.fastmirrorserver.entity
2 |
3 | import com.github.fastmirrorserver.controller.QueryController
4 | import com.github.fastmirrorserver.util.assemblyURL
5 | import org.ktorm.entity.Entity
6 | import org.ktorm.schema.Table
7 | import org.ktorm.schema.boolean
8 | import org.ktorm.schema.datetime
9 | import org.ktorm.schema.varchar
10 | import java.time.LocalDateTime
11 | import javax.servlet.http.HttpServletRequest
12 |
13 | interface Manifest : Entity {
14 | companion object: Entity.Factory()
15 |
16 | val name: String
17 | val mc_version: String
18 | val core_version: String
19 | var update_time: LocalDateTime
20 | val sha1: String
21 | val filename: String
22 | val path: String
23 | var enable: Boolean
24 | }
25 |
26 | fun Manifest.toResponse(request: HttpServletRequest) = mapOf(
27 | "name" to this.name,
28 | "mc_version" to this.mc_version,
29 | "core_version" to this.core_version,
30 | "update_time" to this.update_time,
31 | "sha1" to this.sha1,
32 | "filename" to this.filename,
33 | "download_url" to request.assemblyURL(QueryController.DOWNLOAD, mapOf(
34 | "name" to this.name,
35 | "mc_version" to this.mc_version,
36 | "core_version" to this.core_version,
37 | ))
38 | )
39 |
40 | object Manifests : Table("t_manifest") {
41 | val name = varchar("name").primaryKey().bindTo { it.name }
42 | val mc_version = varchar("mc_version").primaryKey().bindTo { it.mc_version }
43 | val core_version = varchar("core_version").primaryKey().bindTo { it.core_version }
44 | val filename = varchar("filename").bindTo { it.filename }
45 | val enable = boolean("enable").bindTo { it.enable }
46 | val update_time = datetime("update_time").bindTo { it.update_time }
47 | val sha1 = varchar("sha1").bindTo { it.sha1 }
48 | val path = varchar("path").bindTo { it.path }
49 | const val cores_root_path: String = "./core"
50 | }
51 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/fastmirrorserver/entity/Project.kt:
--------------------------------------------------------------------------------
1 | package com.github.fastmirrorserver.entity
2 |
3 | import org.ktorm.entity.Entity
4 | import org.ktorm.schema.Table
5 | import org.ktorm.schema.boolean
6 | import org.ktorm.schema.varchar
7 |
8 | interface Project : Entity {
9 | val id: String
10 | val url: String
11 | val tag: String
12 | val recommend: Boolean
13 | }
14 |
15 | object Projects : Table("t_project") {
16 | val id = varchar("id").primaryKey().bindTo { it.id }
17 | val url = varchar("url").bindTo { it.url }
18 | val tag = varchar("tag").bindTo { it.tag }
19 | val recommend = boolean("recommend").bindTo { it.recommend }
20 | }
21 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/fastmirrorserver/exception/ApiException.kt:
--------------------------------------------------------------------------------
1 | package com.github.fastmirrorserver.exception
2 |
3 | import com.github.fastmirrorserver.dto.ApiResponse
4 | import org.springframework.http.HttpStatus
5 |
6 | class ApiException(
7 | val status: HttpStatus,
8 | val code: String,
9 | message: String,
10 | val data: Any? = null
11 | ) : Throwable(message)
12 | {
13 | fun toResponse() = ApiResponse(
14 | data = data,
15 | code = code,
16 | success = false,
17 | message = message ?: "no further information."
18 | )
19 | fun withData(data: Any) = ApiException(
20 | status = status,
21 | code = code,
22 | message = message ?: "no further information",
23 | data = data
24 | )
25 | companion object {
26 | val AUTH_INVALID_FORMAT = ApiException(
27 | status = HttpStatus.BAD_REQUEST,
28 | code = "err::header::invalid_format",
29 | message = "Format of `Authorization` is invalid. Please use Basic HTTP Authorization."
30 | )
31 | val AUTH_METHOD_NOT_SUPPORTED = ApiException(
32 | status = HttpStatus.BAD_REQUEST,
33 | code = "err::header::not_supported_method",
34 | message = "Method of `Authorization` is not supported. Please use Basic HTTP Authorization."
35 | )
36 | val CONTENT_RANGE_NOT_FOUND = ApiException(
37 | status = HttpStatus.BAD_REQUEST,
38 | code = "err::header::content-range_not_found",
39 | message = "Header `Content-Range` missed."
40 | )
41 | val CONTENT_RANGE_INVALID = ApiException(
42 | status = HttpStatus.BAD_REQUEST,
43 | code = "err::header::content-range_invalid",
44 | message = "Format of `Content-Range` is invalid."
45 | )
46 | val CONTENT_LENGTH_NOT_FOUND = ApiException(
47 | status = HttpStatus.BAD_REQUEST,
48 | code = "err::header::content-length_not_found",
49 | message = "Header `Content-Length` missed."
50 | )
51 |
52 | val ACCOUNT_USERNAME_INVALID = ApiException(
53 | status = HttpStatus.FORBIDDEN,
54 | code = "err::account::invalid_username",
55 | message = "Invalid username."
56 | )
57 | val ACCOUNT_PASSWORD_INVALID = ApiException(
58 | status = HttpStatus.FORBIDDEN,
59 | code = "err::account::invalid_password",
60 | message = "Invalid password."
61 | )
62 | val ACCOUNT_USERNAME_OR_PASSWORD_INVALID = ApiException(
63 | status = HttpStatus.FORBIDDEN,
64 | code = "err::account::invalid_username_password",
65 | message = "Username or password does not match the specification."
66 | )
67 | val PERMISSION_DENIED = ApiException(
68 | status = HttpStatus.UNAUTHORIZED,
69 | code = "err::permission::permission_denied",
70 | message = "Permission denied."
71 | )
72 | val REQUEST_LIMIT = ApiException(
73 | status = HttpStatus.FORBIDDEN,
74 | code = "err::permission::request_limit",
75 | message = "The maximum number of requests is reached."
76 | )
77 |
78 | val ARTIFACT_INFO_NOT_FOUND = ApiException(
79 | status = HttpStatus.NOT_FOUND,
80 | code = "err::info::not_found",
81 | message = "Enquiry not found."
82 | )
83 | val RESOURCE_NOT_FOUND = ApiException(
84 | status = HttpStatus.NOT_FOUND,
85 | code = "err::resource::not_found",
86 | message = "Requested resource does not exist."
87 | )
88 | val UPLOAD_FILE_SIZE_CHANGED = ApiException(
89 | status = HttpStatus.FORBIDDEN,
90 | code = "err::upload::file_size_changed",
91 | message = "File size changed."
92 | )
93 |
94 | val UP_TO_DATE = ApiException(
95 | status = HttpStatus.FORBIDDEN,
96 | code = "err::upload::up_to_date",
97 | message = "Information is up-to-date."
98 | )
99 | val TASK_NOT_FOUND = ApiException(
100 | status = HttpStatus.NOT_FOUND,
101 | code = "err::upload::task_not_found",
102 | message = "Please create task first."
103 | )
104 | val CONFLICT_TASK = ApiException(
105 | status = HttpStatus.CONFLICT,
106 | code = "err::upload::conflict_task",
107 | message = "This task is not finished."
108 | )
109 | val CONFLICT_UPLOAD = ApiException(
110 | status = HttpStatus.CONFLICT,
111 | code = "err::upload::conflict_upload",
112 | message = "This chunk already submitted."
113 | )
114 | val UPLOAD_DATA_INCOMPLETE = ApiException(
115 | status = HttpStatus.FORBIDDEN,
116 | code = "err::upload::data_incomplete",
117 | message = "Mismatch between Content-Length and Range"
118 | )
119 | val TASK_NOT_FINISHED = ApiException(
120 | status = HttpStatus.FORBIDDEN,
121 | code = "err::upload::task_not_finished",
122 | message = "This task is not finished."
123 | )
124 | val NEED_RETRANSMIT = ApiException(
125 | status = HttpStatus.INTERNAL_SERVER_ERROR,
126 | code = "err::upload::need_retransmit",
127 | message = "Unknown error. Please recreate task."
128 | )
129 | }
130 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/fastmirrorserver/interceptor/AuthorizationInterceptor.kt:
--------------------------------------------------------------------------------
1 | package com.github.fastmirrorserver.interceptor
2 |
3 | import com.github.fastmirrorserver.annotation.Authority
4 | import com.github.fastmirrorserver.component.Cache
5 | import com.github.fastmirrorserver.exception.ApiException
6 | import javax.servlet.http.HttpServletRequest
7 | import javax.servlet.http.HttpServletResponse
8 |
9 | import com.github.fastmirrorserver.service.AuthorizationService
10 | import com.github.fastmirrorserver.util.enums.Permission
11 | import com.github.fastmirrorserver.util.UTC
12 | import org.slf4j.LoggerFactory
13 | import org.springframework.beans.factory.annotation.Autowired
14 | import org.springframework.beans.factory.annotation.Value
15 | import org.springframework.stereotype.Component
16 | import org.springframework.web.method.HandlerMethod
17 | import org.springframework.web.servlet.HandlerInterceptor
18 |
19 | @Component
20 | class AuthorizationInterceptor: HandlerInterceptor {
21 | private val logger = LoggerFactory.getLogger(AuthorizationInterceptor::class.java)
22 | @Value("\${spring.profiles.active}")
23 | private lateinit var env: String
24 | @Autowired
25 | private lateinit var authorization: AuthorizationService
26 | @Autowired
27 | private lateinit var cache: Cache
28 |
29 | override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean {
30 | val authority = if(handler !is HandlerMethod) Authority(Permission.NONE)
31 | else handler.method.getAnnotation(Authority::class.java) ?: Authority(Permission.NONE)
32 | val permission = authority.permission
33 |
34 | val traceback = authorization.verification(request, response)
35 |
36 | if(!request.requestURI.startsWith("/api/v3/upload/session/file"))
37 | logger.info("request detected. session=$traceback, method=${request.method}, path=${request.requestURI}")
38 |
39 | if(!(env == "debug" && traceback.permission == Permission.TESTER && authority.ignore_in_debug)) {
40 | if (permission == Permission.TESTER && traceback.permission != Permission.TESTER)
41 | throw ApiException.PERMISSION_DENIED
42 | if (permission.level > traceback.permission.level)
43 | throw ApiException.PERMISSION_DENIED
44 | }
45 |
46 | val requestable = traceback.requestable()
47 |
48 | if(!traceback.permission.unlimited_request) {
49 | response.setIntHeader("x-ratelimit-limit", traceback.permission.request_limit)
50 | response.setIntHeader("x-ratelimit-remaining", traceback.remain_request_count)
51 | response.setHeader("x-ratelimit-reset", traceback.next_refresh_time.UTC)
52 | }
53 |
54 | cache.upd(traceback.token, traceback, traceback.lifetime)
55 | cache.upd(traceback.user, traceback.token, traceback.lifetime)
56 | cache.upd(traceback.remote_address, traceback.token, traceback.lifetime)
57 |
58 | if(requestable) return true
59 |
60 | throw ApiException.REQUEST_LIMIT
61 | }
62 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/fastmirrorserver/interceptor/ResponseResultInterceptor.kt:
--------------------------------------------------------------------------------
1 | package com.github.fastmirrorserver.interceptor
2 |
3 | import com.github.fastmirrorserver.annotation.RawResponse
4 | import org.springframework.stereotype.Component
5 | import org.springframework.web.method.HandlerMethod
6 | import org.springframework.web.servlet.HandlerInterceptor
7 | import javax.servlet.http.HttpServletRequest
8 | import javax.servlet.http.HttpServletResponse
9 |
10 | @Component
11 | class ResponseResultInterceptor : HandlerInterceptor {
12 | override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean {
13 | if(handler !is HandlerMethod) return true
14 |
15 | if(handler.beanType.isAnnotationPresent(RawResponse::class.java))
16 | request.setAttribute("DISABLE_RESPONSE_WRAPPER", RawResponse::class.java)
17 | else if(handler.method.isAnnotationPresent(RawResponse::class.java))
18 | request.setAttribute("DISABLE_RESPONSE_WRAPPER", RawResponse::class.java)
19 | return true
20 | }
21 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/fastmirrorserver/pojo/AccountPOJO.kt:
--------------------------------------------------------------------------------
1 | package com.github.fastmirrorserver.pojo
2 |
3 | import com.github.fastmirrorserver.util.enums.Permission
4 |
5 | data class AccountPOJO(
6 | val username: String,
7 | val password: String,
8 | val permission: Permission
9 | )
10 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/fastmirrorserver/pojo/ManifestPOJO.kt:
--------------------------------------------------------------------------------
1 | package com.github.fastmirrorserver.pojo
2 |
3 | import com.github.fastmirrorserver.dto.Metadata
4 | import com.github.fastmirrorserver.entity.Manifests
5 | import java.time.LocalDateTime
6 |
7 | open class ManifestPOJO(
8 | name: String,
9 | mc_version: String,
10 | core_version: String,
11 | val update_time: LocalDateTime,
12 | val sha1: String,
13 | filetype: String
14 | ): Metadata(name, mc_version, core_version)
15 | {
16 | val filename = "$name-$mc_version-$core_version.$filetype"
17 | val filepath = "${Manifests.cores_root_path}/$name/$mc_version/$filename"
18 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/fastmirrorserver/service/AuthorizationService.kt:
--------------------------------------------------------------------------------
1 | package com.github.fastmirrorserver.service
2 |
3 | import com.github.fastmirrorserver.component.Cache
4 | import com.github.fastmirrorserver.dto.Traceback
5 | import com.github.fastmirrorserver.entity.Account
6 | import com.github.fastmirrorserver.entity.Accounts
7 | import com.github.fastmirrorserver.exception.ApiException
8 | import com.github.fastmirrorserver.pojo.AccountPOJO
9 | import com.github.fastmirrorserver.util.*
10 | import com.github.fastmirrorserver.util.enums.Permission
11 | import org.ktorm.database.Database
12 | import org.ktorm.dsl.eq
13 | import org.ktorm.entity.firstOrNull
14 | import org.ktorm.support.postgresql.bulkInsertOrUpdate
15 | import org.springframework.beans.factory.annotation.Autowired
16 | import org.springframework.stereotype.Service
17 | import java.time.LocalDateTime
18 | import javax.servlet.http.HttpServletRequest
19 | import javax.servlet.http.HttpServletResponse
20 |
21 | @Service
22 | class AuthorizationService {
23 | @Autowired
24 | private lateinit var database: Database
25 | @Autowired
26 | private lateinit var cache: Cache
27 | private val regex = Regex("""^(?[A-Za-z0-9_-]{4,32}):(?[^:\\ /]{8,32})$""")
28 | private fun getAccount(authorization: String): Account {
29 | val match = regex.find(authorization.b64decode().trim())
30 | ?: throw ApiException.AUTH_INVALID_FORMAT
31 | val username = match.groups["username"]?.value
32 | ?: throw ApiException.AUTH_INVALID_FORMAT
33 | val password = match.groups["password"]?.value
34 | ?: throw ApiException.AUTH_INVALID_FORMAT
35 |
36 | return database.accounts.firstOrNull { it.name eq username }?.also {
37 | if(!it.verify(password)) throw ApiException.ACCOUNT_PASSWORD_INVALID
38 | it.last_login = LocalDateTime.now()
39 | it.flushChanges()
40 | } ?: throw ApiException.ACCOUNT_USERNAME_INVALID
41 | }
42 |
43 | /**
44 | * 验证请求是否合法
45 | * 同时检查账号密码(如果有)和session.
46 | */
47 | fun verification(request: HttpServletRequest, response: HttpServletResponse): Traceback {
48 | try { request.getAttribute("SESSION_ENTITY")?.let { return it as Traceback } } catch (_: Exception) { }
49 | val ip = request.remoteAddr
50 | val account = request.authorization?.let { getAccount(authorization = it) }
51 | val cookie = request.cookies?.firstOrNull { it.name == Traceback.COOKIE_NAME }
52 | val token = cookie?.value
53 | ?: account?.let { cache.get(it.name, String::class.java) }
54 | ?: cache.get(ip, String::class.java)
55 |
56 | val (username, permission) = account?.let { it.name to it.permission } ?: ("guest" to Permission.NONE)
57 |
58 | val traceback = token?.let {
59 | cache.get(it, Traceback::class.java)?.let { session -> if(session.user != username) null else session }
60 | } ?: Traceback(username, ip, permission)
61 |
62 | request.setAttribute("SESSION_ENTITY", traceback)
63 | if(cookie == null) response.addCookie(traceback.toCookie())
64 | return traceback
65 | }
66 |
67 | fun registerOrUpdate(pojo: AccountPOJO) {
68 | if(!regex.matches("${pojo.username}:${pojo.password}"))
69 | throw ApiException.ACCOUNT_USERNAME_OR_PASSWORD_INVALID
70 | val method = "SHA256"
71 | val salt = secureRandomString(16)
72 | val signature = "${pojo.password}:$salt".signature(method)
73 |
74 | val authorization_string = "\$$method:$signature:$salt"
75 |
76 | database.bulkInsertOrUpdate(Accounts) {
77 | item {
78 | set(it.name, pojo.username)
79 | set(it.authorization_string, authorization_string)
80 | set(it.permission, pojo.permission)
81 | }
82 | onConflict {
83 | set(it.authorization_string, authorization_string)
84 | set(it.permission, pojo.permission)
85 | }
86 | }
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/fastmirrorserver/service/ErrorReportService.kt:
--------------------------------------------------------------------------------
1 | package com.github.fastmirrorserver.service
2 |
3 | import com.github.fastmirrorserver.util.UTC
4 | import com.github.fastmirrorserver.util.toHtml
5 | import com.github.fastmirrorserver.util.uuid
6 | import org.springframework.beans.factory.annotation.Value
7 | import org.springframework.stereotype.Service
8 | import java.io.File
9 | import java.io.PrintWriter
10 | import java.time.LocalDateTime
11 | import javax.annotation.PostConstruct
12 |
13 | @Service
14 | class ErrorReportService {
15 | @Value("\${server.error-report.path}")
16 | private lateinit var report_root_path: String
17 |
18 | @PostConstruct
19 | private fun initialization() {
20 | File(report_root_path).mkdirs()
21 | }
22 |
23 | private fun File.readAsHtml(): String {
24 | val lines = readLines()
25 | return lines.drop(1).toHtml("Error Report", lines.first())
26 | }
27 |
28 | fun set(err: Throwable): String {
29 | val id = uuid()
30 | val file = File("$report_root_path/$id")
31 | file.createNewFile()
32 | if(file.exists())
33 | file.bufferedWriter().use {
34 | it.write(LocalDateTime.now().UTC)
35 | it.newLine()
36 | it.write("${err::class.java.canonicalName}: ${err.message}")
37 | it.newLine()
38 | PrintWriter(it).also {writer ->
39 | err.printStackTrace(writer)
40 | }.flush()
41 | }
42 | return id
43 | }
44 |
45 | fun get(id: String): String {
46 | val file = File("$report_root_path/$id")
47 | return if(file.exists())
48 | file.readAsHtml()
49 | else
50 | arrayOf("error report not found.").toHtml("Error Report", LocalDateTime.now().UTC)
51 | }
52 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/fastmirrorserver/service/FileService.kt:
--------------------------------------------------------------------------------
1 | package com.github.fastmirrorserver.service
2 |
3 | import com.github.fastmirrorserver.exception.ApiException
4 | import com.github.fastmirrorserver.util.getSpecificArtifact
5 | import org.ktorm.database.Database
6 | import org.springframework.beans.factory.annotation.Autowired
7 | import org.springframework.core.io.InputStreamResource
8 | import org.springframework.http.MediaType
9 | import org.springframework.http.ResponseEntity
10 | import org.springframework.stereotype.Service
11 | import java.io.File
12 | import java.io.FileInputStream
13 | import javax.servlet.http.HttpServletRequest
14 |
15 | @Service
16 | class FileService {
17 | @Autowired
18 | private lateinit var database: Database
19 |
20 | fun send(name: String, core_version: String, mc_version: String): ResponseEntity {
21 | val info = database.getSpecificArtifact(name, mc_version, core_version)
22 | val file = File(info.path)
23 |
24 | if(!file.exists()) throw ApiException.RESOURCE_NOT_FOUND
25 |
26 | return ResponseEntity.ok()
27 | .header("Content-Disposition", "attachment; filename=${info.filename}")
28 | .header("Cache-Control", "no-cache, no-store, must-revalidate")
29 | .header("Pragma", "no-cache")
30 | .header("Expires", "0")
31 | .contentType(MediaType.APPLICATION_OCTET_STREAM)
32 | .contentLength(file.length())
33 | .body(InputStreamResource(FileInputStream(file)))
34 | }
35 |
36 | fun receive(name: String, core_version: String, mc_version: String, request: HttpServletRequest) {
37 |
38 | }
39 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/fastmirrorserver/service/UploadTaskService.kt:
--------------------------------------------------------------------------------
1 | package com.github.fastmirrorserver.service
2 |
3 | import com.github.fastmirrorserver.pojo.ManifestPOJO
4 | import com.github.fastmirrorserver.dto.Metadata
5 | import com.github.fastmirrorserver.entity.Manifest
6 | import com.github.fastmirrorserver.entity.Manifests
7 | import com.github.fastmirrorserver.exception.ApiException
8 | import com.github.fastmirrorserver.util.*
9 | import org.ktorm.database.Database
10 | import org.ktorm.entity.firstOrNull
11 | import org.ktorm.support.postgresql.insertOrUpdate
12 | import org.slf4j.LoggerFactory
13 | import org.springframework.beans.factory.annotation.Autowired
14 | import org.springframework.stereotype.Service
15 | import javax.servlet.http.HttpServletRequest
16 |
17 | @Service
18 | class UploadTaskService {
19 | private val logger = LoggerFactory.getLogger(this::class.java)
20 | @Autowired
21 | private lateinit var database: Database
22 |
23 | private val tasks = UploadTaskContainer()
24 |
25 | private fun insertOrUpdate(pojo: ManifestPOJO) = database.insertOrUpdate(Manifests) {
26 | set(it.name, pojo.name)
27 | set(it.mc_version, pojo.mc_version)
28 | set(it.core_version, pojo.core_version)
29 | set(it.update_time, pojo.update_time)
30 | set(it.sha1, pojo.sha1)
31 | set(it.filename, pojo.filename)
32 | set(it.path, pojo.filepath)
33 | set(it.enable, false)
34 | onConflict {
35 | set(it.update_time, pojo.update_time)
36 | set(it.sha1, pojo.sha1)
37 | set(it.filename, pojo.filename)
38 | set(it.path, pojo.filepath)
39 | set(it.enable, false)
40 | }
41 | }
42 |
43 | private fun isEquivalent(pojo: ManifestPOJO, manifest: Manifest)
44 | = pojo.update_time == manifest.update_time
45 | && pojo.sha1 == manifest.sha1
46 |
47 | fun createTask(pojo: ManifestPOJO, request: HttpServletRequest): Map {
48 | val entity = database.all_cores
49 | .querySpecificArtifact(pojo)
50 | .firstOrNull()
51 | // 有记录 任务列表无 已启用 数据有更新
52 | if(entity == null) // 否 -- -- -- 创建任务
53 | insertOrUpdate(pojo)
54 | else if(tasks.has(pojo)) // 是 否 -- -- 抛异常
55 | // throw ApiException.CONFLICT_TASK
56 | tasks.removeTask(pojo)
57 | else if(!entity.enable) // 是 是 否 -- 创建任务
58 | insertOrUpdate(pojo)
59 | else if(isEquivalent(pojo, entity)) // 是 是 是 否 抛异常
60 | throw ApiException.UP_TO_DATE
61 | else if(entity.sha1 == pojo.sha1 && entity.enable) {
62 | entity.update_time = pojo.update_time
63 | entity.flushChanges()
64 | return mapOf(
65 | "upload_uri" to null,
66 | "expiration_time" to null,
67 | "api_type" to "fastmirror.v3"
68 | )
69 | }
70 | else insertOrUpdate(pojo) // 是 是 是 是 创建任务
71 |
72 | val task = tasks.createTask(pojo, pojo.uploadUrl(request)) ?: throw ApiException.CONFLICT_TASK
73 |
74 | return mapOf(
75 | "upload_uri" to task.uri,
76 | "expiration_time" to task.expired.UTC,
77 | "api_type" to "fastmirror.v3"
78 | )
79 | }
80 |
81 | private val range_regex = Regex("""^bytes[ =](?\d+)?-(?\d+)?/(?\d+)$""")
82 | fun uploadFile(name: String, mc_version: String, core_version: String, request: HttpServletRequest): Map {
83 | val tuple = Metadata(name, mc_version, core_version)
84 |
85 | val length = request.getIntHeader("Content-Length").let {
86 | if(it > 0) it else throw ApiException.CONTENT_LENGTH_NOT_FOUND
87 | }.toLong()
88 | val (start, end, size) = request.run {
89 | val range = getHeader("Content-Range") ?: throw ApiException.CONTENT_RANGE_NOT_FOUND
90 | val match = range_regex.find(range.trim())?.groups ?: throw ApiException.CONTENT_RANGE_INVALID
91 | val size = match["size"]?.value?.toLong() ?: throw ApiException.CONTENT_RANGE_INVALID
92 | val start = match["start"]?.value?.toLong() ?: 0
93 | val end = match["end"]?.value?.toLong() ?: size
94 | Triple(start, end, size)
95 | }
96 |
97 | if(start < 0 || start >= end || end > size || start + length - 1 != end)
98 | throw ApiException.CONTENT_RANGE_INVALID
99 |
100 | val task = tasks.getTask(tuple)
101 | val stream = request.inputStream
102 |
103 | try { task.write(stream, start, length, size) }
104 | catch (e: ApiException) {
105 | throw e.withData(mapOf(
106 | "next_expected_range" to task.nextExpectedRange(),
107 | "expired_time" to task.expired.`GMT+8`
108 | ))
109 | }
110 |
111 | return mapOf(
112 | "next_expected_range" to task.nextExpectedRange(2),
113 | "expired_time" to task.expired.`GMT+8`
114 | )
115 | }
116 |
117 | fun closeTask(name: String, mc_version: String, core_version: String) {
118 | val tuple = Metadata(name, mc_version, core_version)
119 | val status = tasks.closeTask(tuple)
120 |
121 | if(!status) throw ApiException.TASK_NOT_FINISHED
122 |
123 | val core = database.all_cores.querySpecificArtifact(tuple).firstOrNull() ?: throw ApiException.NEED_RETRANSMIT
124 | core.enable = true
125 | core.flushChanges()
126 | logger.info("info flushed.")
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/fastmirrorserver/util/Crypto.kt:
--------------------------------------------------------------------------------
1 | package com.github.fastmirrorserver.util
2 |
3 | import java.security.MessageDigest
4 | import java.security.SecureRandom
5 |
6 | private val digests = mutableMapOf(
7 | "SHA1" to MessageDigest.getInstance("SHA-1"),
8 | "SHA256" to MessageDigest.getInstance("SHA-256")
9 | )
10 | private val sequence = "abcdefghijklmnopqrstuvwxy0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZz"
11 | private val secureRandom = SecureRandom()
12 |
13 | fun ByteArray.signature(digest: String = "SHA1") = digests[digest]!!.digest(this).hex()
14 | fun String.signature(digest: String) = this.toByteArray().signature(digest)
15 |
16 | fun secureRandomString(len: Int): String {
17 | val str = CharArray(len)
18 | for(i in 0 until len)
19 | str[i] = sequence[secureRandom.nextInt(sequence.length)]
20 | return String(str)
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/fastmirrorserver/util/Database.kt:
--------------------------------------------------------------------------------
1 | package com.github.fastmirrorserver.util
2 |
3 | import com.github.fastmirrorserver.dto.*
4 | import com.github.fastmirrorserver.entity.*
5 | import com.github.fastmirrorserver.exception.ApiException
6 | import org.ktorm.database.Database
7 | import org.ktorm.dsl.*
8 | import org.ktorm.entity.*
9 | import org.ktorm.expression.ArgumentExpression
10 | import org.ktorm.schema.BooleanSqlType
11 | import org.ktorm.schema.ColumnDeclaring
12 |
13 | fun Project.toSummary() = Summary(this.id, this.tag, this.recommend)
14 |
15 | fun Manifest.toArtifactSummary()
16 | = mapOf(
17 | "name" to this.name,
18 | "mc_version" to this.mc_version,
19 | "core_version" to this.core_version,
20 | "update_time" to this.update_time,
21 | "sha1" to this.sha1
22 | )
23 |
24 | fun EntitySequence.toProjectMcVersionSummary(offset: Int, limit: Int)
25 | = mapOf(
26 | "builds" to this.drop(offset).take(limit).map { it.toArtifactSummary() },
27 | "offset" to offset,
28 | "limit" to limit,
29 | "count" to this.totalRecords
30 | )
31 |
32 | val Database.all_cores get() = sequenceOf(Manifests)
33 | .sortedBy({ it.name.asc() }, { it.mc_version.desc() }, { it.update_time.desc() })
34 | val Database.available_cores get() = all_cores.filter { it.enable }
35 |
36 | val Database.accounts get() = sequenceOf(Accounts)
37 |
38 |
39 | fun EntitySequence.queryAllCoreVersionOfMcVersion(name: String, mc_version: String)
40 | = filter { it.name eq name }.filter { it.mc_version eq mc_version }
41 | fun EntitySequence.querySpecificArtifact(name: String, mc_version: String, core_version:String)
42 | = queryAllCoreVersionOfMcVersion(name, mc_version).filter { it.core_version eq core_version }
43 | fun EntitySequence.querySpecificArtifact(tuple: Metadata)
44 | = querySpecificArtifact(tuple.name, tuple.mc_version, tuple.core_version)
45 |
46 | private fun Database.queryProjectBy(condition: () -> ColumnDeclaring)
47 | = from(Manifests).leftJoin(Projects, on = Projects.id eq Manifests.name)
48 | .selectDistinct(Manifests.name, Manifests.mc_version, Projects.url, Projects.tag, Projects.recommend)
49 | .where { Manifests.enable }
50 | .where (condition)
51 | .orderBy(Manifests.name.asc(), Manifests.mc_version.desc())
52 | .mapNotNull{
53 | val project = it[Manifests.name] ?: return@mapNotNull null
54 | val homepage = it[Projects.url] ?: return@mapNotNull null
55 | val tag = it[Projects.tag] ?: return@mapNotNull null
56 | val recommend = it[Projects.recommend] ?: return@mapNotNull null
57 | val version = it[Manifests.mc_version] ?: return@mapNotNull null
58 | return@mapNotNull Tuple4(project, tag, homepage, recommend) to version
59 | }.groupBy { it.first }
60 | .mapValues { entry -> entry.value.map { it.second } }
61 | .map { mapOf(
62 | "name" to it.key.element1,
63 | "tag" to it.key.element2,
64 | "homepage" to it.key.element3,
65 | "recommend" to it.key.element4,
66 | "mc_versions" to it.value
67 | ) }
68 |
69 | fun Database.getAllProjects() = queryProjectBy { ArgumentExpression(true, BooleanSqlType) }
70 |
71 | fun Database.getSupportedMcVersionOfProjects(projects: ArrayList) = queryProjectBy {
72 | if(projects.any())
73 | projects.map { it eq Manifests.name }.reduce { a, b -> a or b }
74 | else
75 | ArgumentExpression(false, BooleanSqlType)
76 | }
77 |
78 | fun Database.getSupportedMcVersionOfProject(name: String) = queryProjectBy { Manifests.name eq name }
79 | .firstOrNull() ?: throw ApiException.ARTIFACT_INFO_NOT_FOUND
80 |
81 | fun Database.getAllCoreVersionOfMcVersion(name: String, mc_version: String, offset: Int?, limit: Int?)
82 | = available_cores.queryAllCoreVersionOfMcVersion(name, mc_version)
83 | .toProjectMcVersionSummary(
84 | offset = if(offset == null) 0 else if (offset < 0) 0 else offset,
85 | limit = if(limit == null) 1 else if (limit > 25) 25 else if (limit <= 0) 1 else limit
86 | )
87 |
88 | fun Database.getSpecificArtifact(name: String, mc_version: String, core_version:String)
89 | = try {
90 | available_cores.querySpecificArtifact(name, mc_version, core_version)
91 | .first()
92 | }catch (e: NoSuchElementException) {
93 | throw ApiException.ARTIFACT_INFO_NOT_FOUND
94 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/fastmirrorserver/util/FileWriter.kt:
--------------------------------------------------------------------------------
1 | package com.github.fastmirrorserver.util
2 |
3 | import com.github.fastmirrorserver.exception.ApiException
4 | import java.io.File
5 | import java.io.InputStream
6 | import java.io.RandomAccessFile
7 | import java.nio.channels.Channels
8 | import java.nio.channels.FileChannel
9 |
10 | class FileWriter(private val file: File, val size: Long) {
11 | private val records = ArrayList()
12 | private val tmp_file = File("./tmp/${file.name}.tmp")
13 | private val random_access_file: RandomAccessFile
14 | private val channel: FileChannel
15 |
16 | private var written_size = 0L
17 |
18 | private var released: Boolean = false
19 |
20 | init {
21 | if(tmp_file.exists()) tmp_file.delete()
22 | tmp_file.parentFile.mkdirs()
23 | random_access_file = RandomAccessFile(tmp_file, "rw")
24 | channel = random_access_file.channel
25 | }
26 |
27 | private fun recorded(start: Long, end: Long) {
28 | val idx = records.indexOfLast { it.end < start }
29 |
30 | val has_prev = idx != -1
31 | val has_next = idx + 1 < records.size
32 | val can_merged_with_prev = has_prev && records[idx].end + 1 == start
33 | val can_merged_with_next = has_next && records[idx + 1].start - 1 == end
34 |
35 | if(has_next && records[idx + 1].start <= end)
36 | throw ApiException.CONFLICT_UPLOAD
37 |
38 | if(can_merged_with_next && can_merged_with_prev) {
39 | records[idx].end = records[idx + 1].end
40 | records.removeAt(idx + 1)
41 | }
42 | else if(can_merged_with_prev) records[idx].end = end
43 | else if(can_merged_with_next) records[idx + 1].start = start
44 | else records.add(idx + 1, Record(start, end))
45 | }
46 |
47 | fun nextExpectedRange(maximum: Int = Int.MAX_VALUE): List {
48 | val list = ArrayList()
49 | if(records[0].start != 0L) list.add("0-${records[0].start - 1}")
50 | for(idx in 0 until (list.size - 1).coerceAtMost(maximum - 1))
51 | list.add("${records[idx].end + 1}-${records[idx + 1].start - 1}")
52 | if(records.size > 1 && list.size < maximum) records[records.size - 1].run {
53 | if(end + 1 < size) list.add("${end + 1}-${size - 1}")
54 | }
55 | return list
56 | }
57 |
58 | fun write(stream: InputStream, position: Long, length: Long): Long {
59 | synchronized(this) { recorded(position, position + length - 1) }
60 |
61 | val read_chan = Channels.newChannel(stream)
62 | val written = channel.transferFrom(read_chan, position, length)
63 | read_chan.close()
64 |
65 | written_size += written
66 | return written
67 | }
68 |
69 | // fun write(data: ByteBuffer, start: Long, end: Long): Long {
70 | // if(data.position() != 0) data.flip()
71 | // if(data.limit() + start - 1 != end)
72 | // throw ApiException.UPLOAD_DATA_INCOMPLETE
73 | //
74 | // synchronized(this) { recorded(start, end) }
75 | //
76 | // var written = 0L
77 | // while (data.hasRemaining()) {
78 | // written += channel.write(data, written + start)
79 | // data.compact()
80 | // data.flip()
81 | // }
82 | //
83 | // written_size += written
84 | // return written
85 | // }
86 |
87 | fun flush(): Boolean {
88 | synchronized(this) {
89 | if(size > written_size) return false
90 | if(released) return false
91 | released = true
92 | }
93 | file.parentFile.mkdirs()
94 | val file_channel = file.outputStream().channel
95 | var written = 0L
96 | while(written < size) {
97 | written += file_channel.transferFrom(channel, written, size - written)
98 | }
99 | internalRelease()
100 | return true
101 | }
102 |
103 | fun release(): Boolean {
104 | internalRelease()
105 | return true
106 | }
107 |
108 | private fun internalRelease() {
109 | channel.close()
110 | random_access_file.close()
111 | tmp_file.delete()
112 | }
113 |
114 | private data class Record(
115 | var start: Long,
116 | var end: Long
117 | )
118 |
119 | class Builder(private val file: File) {
120 | private var size: Long = 0
121 |
122 | fun size(size: Long): Builder { this.size = size; return this }
123 | fun builder(): FileWriter = FileWriter(file, size)
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/fastmirrorserver/util/Json.kt:
--------------------------------------------------------------------------------
1 | package com.github.fastmirrorserver.util
2 |
3 | import com.fasterxml.jackson.annotation.JsonAutoDetect
4 | import com.fasterxml.jackson.annotation.PropertyAccessor
5 | import com.fasterxml.jackson.databind.ObjectMapper
6 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
7 | import com.fasterxml.jackson.module.kotlin.registerKotlinModule
8 |
9 | object Json {
10 | private val mapper = ObjectMapper().also {
11 | it.registerKotlinModule()
12 | it.registerModule(JavaTimeModule())
13 | // it.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY)
14 | it.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY)
15 | it.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY)
16 | }
17 |
18 | fun serialization(value: T): String = mapper.writeValueAsString(value)
19 | fun deserialization(src: String, type: Class): T = mapper.readValue(src, type)
20 | }
21 |
22 | inline fun T.serialize() = Json.serialization(this)
23 | inline fun String.deserialize(): T? = Json.deserialization(this, T::class.java)
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/fastmirrorserver/util/Network.kt:
--------------------------------------------------------------------------------
1 | package com.github.fastmirrorserver.util
2 |
3 | import com.github.fastmirrorserver.exception.ApiException
4 | import javax.servlet.http.HttpServletRequest
5 |
6 | private val regex = Regex("""^(?[A-Z][a-z]+) (?[A-Za-z0-9+/=]+)$""")
7 |
8 | val HttpServletRequest.authorization get() = this
9 | .getHeader("Authorization")?.let {
10 | val match = regex.find(it.trim())
11 | ?: throw ApiException.AUTH_INVALID_FORMAT
12 | val method = match.groups["method"]?.value
13 | ?: throw ApiException.AUTH_INVALID_FORMAT
14 | val body = match.groups["body"]?.value
15 | ?: throw ApiException.AUTH_INVALID_FORMAT
16 |
17 | if(method != "Basic")
18 | throw ApiException.AUTH_METHOD_NOT_SUPPORTED
19 | return@let body
20 | }
21 | val HttpServletRequest.host get() =
22 | if((scheme == "http" && serverPort == 80) ||(scheme == "https" && serverPort == 443))
23 | "$scheme://$serverName"
24 | else
25 | "$scheme://$serverName:$serverPort"
26 | fun HttpServletRequest.assemblyURL(path: String, map: Map)
27 | = "$host$path".template(map)
28 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/fastmirrorserver/util/Time.kt:
--------------------------------------------------------------------------------
1 | package com.github.fastmirrorserver.util
2 |
3 | import java.time.LocalDateTime
4 | import java.time.ZoneId
5 | import java.time.ZonedDateTime
6 | import java.time.format.DateTimeFormatter
7 |
8 | private val zone = ZoneId.systemDefault()
9 | private val ZONE_UTC = ZoneId.of("UTC")
10 | private val `ZONE_GMT+8` = ZoneId.of("Asia/Shanghai")
11 |
12 | val LocalDateTime.UTC get() = this.atZone(zone).format(DateTimeFormatter.ISO_INSTANT)!!
13 | val ZonedDateTime.UTC get() = format(DateTimeFormatter.ISO_INSTANT)!!
14 | val String.UTC get() = ZonedDateTime.parse(this).withZoneSameInstant(ZONE_UTC)
15 |
16 | val LocalDateTime.`GMT+8` get() = this.atZone(zone).withZoneSameInstant(`ZONE_GMT+8`).UTC
17 | fun timestamp() = System.currentTimeMillis() / 1000
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/fastmirrorserver/util/UploadTaskContainer.kt:
--------------------------------------------------------------------------------
1 | package com.github.fastmirrorserver.util
2 |
3 | import com.github.fastmirrorserver.pojo.ManifestPOJO
4 | import com.github.fastmirrorserver.dto.Metadata
5 | import com.github.fastmirrorserver.exception.ApiException
6 | import java.io.File
7 | import java.io.InputStream
8 | import java.time.LocalDateTime
9 | import java.util.TreeMap
10 |
11 | class UploadTaskContainer {
12 | data class Task(
13 | val uri: String,
14 | private val builder: FileWriter.Builder,
15 | val expired: LocalDateTime
16 | ) {
17 | private lateinit var writer: FileWriter
18 | fun isExpired(now: LocalDateTime = LocalDateTime.now()) = expired < now
19 |
20 | fun write(stream: InputStream, position: Long, length: Long, total_size: Long): Long {
21 | if(!this::writer.isInitialized) writer = builder.size(total_size).builder()
22 | if(total_size != writer.size) throw ApiException.UPLOAD_FILE_SIZE_CHANGED
23 | return writer.write(stream, position, length)
24 | }
25 | fun nextExpectedRange(maximum: Int = Int.MAX_VALUE) = writer.nextExpectedRange(maximum)
26 | fun release() { if(this::writer.isInitialized) writer.release() }
27 | fun flush() = writer.flush()
28 | }
29 | private val task_pool: TreeMap = TreeMap()
30 |
31 | fun removeTask(tuple: Metadata) {
32 | task_pool[tuple.key]?.release()
33 | task_pool.remove(tuple.key)
34 | }
35 |
36 | fun createTask(pojo: ManifestPOJO, uri: String): Task? {
37 | if(has(pojo)) return null
38 | val task = Task(
39 | uri = uri,
40 | builder = FileWriter.Builder(File(pojo.filepath)),
41 | expired = LocalDateTime.now().plusMinutes(10)
42 | )
43 | task_pool[pojo.key] = task
44 | return task
45 | }
46 |
47 | fun getTask(tuple: Metadata): Task {
48 | val task = task_pool[tuple.key] ?: throw ApiException.TASK_NOT_FOUND
49 | if(!task.isExpired()) return task
50 | removeTask(tuple)
51 | throw ApiException.TASK_NOT_FOUND
52 | }
53 |
54 | fun closeTask(tuple: Metadata): Boolean {
55 | val status = getTask(tuple).flush()
56 | if(status) removeTask(tuple)
57 | return status
58 | }
59 |
60 | fun has(tuple: Metadata): Boolean {
61 | val v = task_pool[tuple.key] ?: return false
62 | if(!v.isExpired()) return true
63 | removeTask(tuple)
64 | return false
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/fastmirrorserver/util/Utility.kt:
--------------------------------------------------------------------------------
1 | package com.github.fastmirrorserver.util
2 |
3 | import com.github.fastmirrorserver.dto.Metadata
4 | import com.github.fastmirrorserver.entity.Manifest
5 | import java.lang.StringBuilder
6 | import java.math.BigInteger
7 | import java.util.*
8 | import java.util.regex.Pattern
9 |
10 | fun ByteArray.hex() = BigInteger(1, this).toString(16).uppercase()
11 |
12 | fun uuid() = UUID.randomUUID().toString().uppercase()
13 |
14 | fun String.b64encode(): String = String(Base64.getEncoder().encode(this.toByteArray()))
15 | fun String.b64decode(): String = String(Base64.getDecoder().decode(this))
16 |
17 | fun String.template(map: Map): String {
18 | val pattern = Pattern.compile("""\{([A-Za-z0-9_-]+)}""")
19 | val matcher = pattern.matcher(this)
20 | val sb = StringBuilder()
21 | while(matcher.find()) {
22 | val k = matcher.group()
23 | (map[k.substring(1, k.length - 1)] ?: k)
24 | .let { matcher.appendReplacement(sb, it) }
25 | }
26 | matcher.appendTail(sb)
27 | return sb.toString()
28 | }
29 |
30 | fun String.urlTemplate(tuple: Metadata)
31 | = this.template(mapOf(
32 | "name" to tuple.name,
33 | "mc_version" to tuple.mc_version,
34 | "core_version" to tuple.core_version
35 | ))
36 |
37 | private fun Iterator.toHtml(title: String, subtitle: String): String {
38 | val builder = StringBuilder()
39 | builder.append("").append('\n')
40 | .append("").append('\n')
41 | .append("").append('\n')
42 | .append("$title").append('\n')
43 | .append("").append('\n')
44 | .append("").append('\n')
45 | .append("$title
").append('\n')
46 | .append("$subtitle
").append('\n')
47 | while(hasNext()) builder.append("${next()}
").append('\n')
48 | builder.append("").append('\n')
49 | return builder.toString()
50 | }
51 | fun Array.toHtml(title: String, subtitle: String) = iterator().toHtml(title, subtitle)
52 | fun Iterable.toHtml(title: String, subtitle: String) = iterator().toHtml(title, subtitle)
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/fastmirrorserver/util/enums/Permission.kt:
--------------------------------------------------------------------------------
1 | package com.github.fastmirrorserver.util.enums
2 |
3 | enum class Permission(
4 | val level: Int,
5 | val request_limit: Int = 0,
6 | val refresh_period: Int = 0,
7 | val unlimited_request: Boolean = false
8 | ) {
9 | TESTER(0, 3, 10),
10 | NONE(1, 200, 60 * 60),
11 | USER(2, 500, 60 * 60),
12 | COLLECTOR(3, unlimited_request = true),
13 | ROOT(4, unlimited_request = true),
14 | }
--------------------------------------------------------------------------------
/src/main/resources/application.yml:
--------------------------------------------------------------------------------
1 | server:
2 | tomcat:
3 | max-http-form-post-size: 100MB
4 | error-report:
5 | path: "./error-reports"
6 | forward-headers-strategy: native
7 | spring:
8 | profiles:
9 | active: pond
10 | datasource:
11 | driver-class-name: org.postgresql.Driver
12 | servlet:
13 | multipart:
14 | max-file-size: 100MB
15 | max-request-size: 100MB
16 | mvc:
17 | path match:
18 | matching-strategy: ant_path_matcher
19 |
--------------------------------------------------------------------------------
/src/main/resources/table.sql:
--------------------------------------------------------------------------------
1 | create table if not exists t_project (
2 | "id" varchar(31) primary key,
3 | "url" varchar(255) not null,
4 | "tag" varchar(63) not null,
5 | "recommend" boolean not null
6 | );
7 | create table if not exists t_manifest (
8 | "name" varchar(31) references t_project("id"),
9 | "mc_version" varchar(15) not null,
10 | "core_version" varchar(31) not null,
11 | "update_time" timestamp not null,
12 | "sha1" varchar(127) not null,
13 | "filename" varchar(127) not null,
14 | "path" varchar(255) not null,
15 | "enable" boolean not null,
16 | constraint t_manifest_pk primary key ("name", "mc_version", "core_version")
17 | );
18 | create table if not exists t_account (
19 | "name" varchar(20) primary key,
20 | "authorization_string" varchar(255) not null,
21 | "permission" varchar(20) not null,
22 | "last_login" timestamp not null
23 | );
24 |
--------------------------------------------------------------------------------
/src/test/kotlin/com/github/fastmirrorserver/test/AccountTest.kt:
--------------------------------------------------------------------------------
1 | package com.github.fastmirrorserver.test
2 |
3 | import com.github.fastmirrorserver.controller.AccountController
4 | import com.github.fastmirrorserver.controller.TestController
5 | import com.github.fastmirrorserver.dto.ApiResponse
6 | import com.github.fastmirrorserver.exception.ApiException
7 | import com.github.fastmirrorserver.pojo.AccountPOJO
8 | import com.github.fastmirrorserver.util.*
9 | import com.github.fastmirrorserver.util.enums.Permission
10 | import org.junit.jupiter.api.MethodOrderer
11 | import org.junit.jupiter.api.Order
12 | import org.junit.jupiter.api.Test
13 | import org.junit.jupiter.api.TestMethodOrder
14 | import org.springframework.beans.factory.annotation.Autowired
15 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
16 | import org.springframework.boot.test.context.SpringBootTest
17 | import org.springframework.http.MediaType
18 | import org.springframework.test.web.servlet.MockMvc
19 | import org.springframework.test.web.servlet.get
20 | import org.springframework.test.web.servlet.post
21 |
22 | @SpringBootTest
23 | @AutoConfigureMockMvc
24 | @TestMethodOrder(MethodOrderer.OrderAnnotation::class)
25 | class AccountTest {
26 | @Autowired
27 | private lateinit var mock: MockMvc
28 |
29 | companion object {
30 | private val username = "tester"
31 | private val password = secureRandomString(16)//"vn,jtJdjh5UXyVCh5y=.Cz7^"
32 | }
33 |
34 | // @Test
35 | // fun `upload bytes, expect successful`() {
36 | // val data = "580BA888-81FE-4D47-BFF6-2C770E2CD811".toByteArray()
37 | //
38 | // mock.put(TestController.UPLOAD_TEST) {
39 | // headers {
40 | // set("Range", "0-15/15")
41 | // set("Content-Length", data.size.toString())
42 | // }
43 | // content = data
44 | // }.andDo { print() }
45 | // .andExpect {
46 | // status { is2xxSuccessful() }
47 | // content { data.signature() }
48 | // }
49 | // }
50 |
51 | @Test
52 | @Order(0)
53 | fun `register, expect successful`() {
54 | println(password)
55 | mock.post(AccountController.REGISTER) {
56 | contentType = MediaType.APPLICATION_JSON
57 | content = AccountPOJO(username, password, Permission.TESTER).serialize()
58 | }
59 | // .andDo { print() }
60 | .andExpect {
61 | status { is2xxSuccessful() }
62 | }
63 | }
64 | @Test
65 | @Order(0)
66 | fun `register but invalid password, expect failed`() {
67 | mock.post(AccountController.REGISTER) {
68 | contentType = MediaType.APPLICATION_JSON
69 | content = AccountPOJO("username", "q:23", Permission.TESTER).serialize()
70 | }
71 | .andExpect {
72 | status { isForbidden() }
73 | content { json(ApiException.ACCOUNT_USERNAME_OR_PASSWORD_INVALID.toResponse().serialize()) }
74 | }
75 | }
76 | @Test
77 | @Order(1)
78 | fun `login, expect successful`() {
79 | mock.get(TestController.DEV_NULL) {
80 | header("Authorization", "Basic ${"$username:$password".b64encode()}")
81 | }
82 | // .andDo { print() }
83 | .andExpect {
84 | status { is2xxSuccessful() }
85 | content {
86 | json(ApiResponse.success().serialize())
87 | }
88 | }
89 | }
90 |
91 | @Test
92 | @Order(1)
93 | fun `permission, expect failed`() {
94 | mock.get(TestController.PERMISSION_TEST) {
95 | header("Authorization", "Basic ${"$username:$password".b64encode()}")
96 | }
97 | // .andDo { print() }
98 | .andExpect {
99 | status { isUnauthorized() }
100 | content {
101 | json(ApiException.PERMISSION_DENIED.toResponse().serialize())
102 | }
103 | }
104 | }
105 |
106 | @Test
107 | @Order(2)
108 | fun `login but wrong password, expect failed`() {
109 | mock.get(TestController.DEV_NULL) {
110 | header("Authorization", "Basic ${"$username:wrong_password".b64encode()}")
111 | }
112 | // .andDo { print() }
113 | .andExpect {
114 | status { isForbidden() }
115 | content {
116 | json(ApiException.ACCOUNT_PASSWORD_INVALID.toResponse().serialize())
117 | }
118 | }
119 | }
120 |
121 | @Test
122 | @Order(2)
123 | fun `login but wrong username, expect failed`() {
124 | mock.get(TestController.DEV_NULL) {
125 | header("Authorization", "Basic ${"wrong_username:wrong_password".b64encode()}")
126 | }
127 | // .andDo { print() }
128 | .andExpect {
129 | status { isForbidden() }
130 | content {
131 | json(ApiException.ACCOUNT_USERNAME_INVALID.toResponse().serialize())
132 | }
133 | }
134 | }
135 |
136 | @Test
137 | @Order(2)
138 | fun `illegal format, except failed`() {
139 | mock.get(TestController.DEV_NULL) {
140 | header("Authorization", "Basic-${"$username+$password".b64encode()}")
141 | }
142 | // .andDo { print() }
143 | .andExpect {
144 | status { isBadRequest() }
145 | content {
146 | json(ApiException.AUTH_INVALID_FORMAT.toResponse().serialize())
147 | }
148 | }
149 | }
150 |
151 | @Test
152 | @Order(2)
153 | fun `empty or blank Authorization, except failed`() {
154 | mock.get(TestController.DEV_NULL) {
155 | header("Authorization", " ")
156 | }
157 | // .andDo { print() }
158 | .andExpect {
159 | status { isBadRequest() }
160 | content {
161 | json(ApiException.AUTH_INVALID_FORMAT.toResponse().serialize())
162 | }
163 | }
164 | }
165 |
166 | @Test
167 | @Order(2)
168 | fun `not support Authorization method, expect failed`() {
169 | mock.get(TestController.DEV_NULL) {
170 | header("Authorization", "Bearer nullptr")
171 | }
172 | // .andDo { print() }
173 | .andExpect {
174 | status { isBadRequest() }
175 | content {
176 | json(ApiException.AUTH_METHOD_NOT_SUPPORTED.toResponse().serialize())
177 | }
178 | }
179 | }
180 | @Test
181 | @Order(2)
182 | fun `login but invalid format, expect failed`() {
183 | mock.get(TestController.DEV_NULL) {
184 | header("Authorization", "Basic ${"$username+$password".b64encode()}")
185 | }
186 | // .andDo { print() }
187 | .andExpect {
188 | status { isBadRequest() }
189 | content {
190 | json(ApiException.AUTH_INVALID_FORMAT.toResponse().serialize())
191 | }
192 | }
193 | }
194 |
195 | @Test
196 | @Order(3)
197 | fun `request limit, expect failed`() {
198 | mock.get(TestController.DEV_NULL) {
199 | header("Authorization", "Basic ${"$username:$password".b64encode()}")
200 | }.andExpect { status { is2xxSuccessful() } }
201 | mock.get(TestController.DEV_NULL) {
202 | header("Authorization", "Basic ${"$username:$password".b64encode()}")
203 | }
204 | mock.get(TestController.DEV_NULL) {
205 | header("Authorization", "Basic ${"$username:$password".b64encode()}")
206 | }
207 | // .andDo { print() }
208 | .andExpect {
209 | status { isForbidden() }
210 | content {
211 | json(ApiException.REQUEST_LIMIT.toResponse().serialize())
212 | }
213 | }
214 | }
215 | }
216 |
--------------------------------------------------------------------------------
/src/test/kotlin/com/github/fastmirrorserver/test/UtilityTest.kt:
--------------------------------------------------------------------------------
1 | package com.github.fastmirrorserver.test
2 |
3 | import com.github.fastmirrorserver.dto.Metadata
4 | import com.github.fastmirrorserver.util.template
5 | import com.github.fastmirrorserver.util.urlTemplate
6 | import org.junit.jupiter.api.Test
7 | import org.springframework.boot.test.context.SpringBootTest
8 | import org.springframework.util.Assert
9 |
10 | @SpringBootTest
11 | class UtilityTest {
12 | @Test
13 | fun `String template by map, expect success`() {
14 | val ans = "/api/v3/download/{name}/{mc_version}/{core_version}".template(mapOf(
15 | "name" to "Arclight",
16 | "mc_version" to "1.19.1",
17 | "core_version" to "1.0.0"
18 | ))
19 | Assert.isTrue(ans == "/api/v3/download/Arclight/1.19.1/1.0.0", ans)
20 | }
21 | @Test
22 | fun `String template by object, expect success`() {
23 | val ans = "/api/v3/download/{name}/{mc_version}/{core_version}".urlTemplate(
24 | Metadata(
25 | name = "Arclight",
26 | mc_version = "1.19.1",
27 | core_version = "1.0.0"
28 | )
29 | )
30 | Assert.isTrue(ans == "/api/v3/download/Arclight/1.19.1/1.0.0", ans)
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/test/resources/application.yml:
--------------------------------------------------------------------------------
1 | server:
2 | tomcat:
3 | max-http-form-post-size: 100MB
4 | forward-headers-strategy: native
5 | spring:
6 | profiles:
7 | active: debug
8 | datasource:
9 | driver-class-name: org.postgresql.Driver
10 | servlet:
11 | multipart:
12 | max-file-size: 100MB
13 | max-request-size: 100MB
14 | mvc:
15 | path match:
16 | matching-strategy: ant_path_matcher
17 |
--------------------------------------------------------------------------------
/src/test/resources/table.sql:
--------------------------------------------------------------------------------
1 | create table if not exists t_project (
2 | "name" varchar(31) primary key,
3 | "url" varchar(255) not null,
4 | "tag" varchar(63) not null,
5 | "recommend" boolean not null
6 | );
7 | create table if not exists t_core (
8 | "name" varchar(31) references t_project("name"),
9 | "mc_version" varchar(15) not null,
10 | "core_version" varchar(31) not null,
11 | "update_time" timestamp not null,
12 | "sha1" varchar(127) not null,
13 | "filetype" varchar(31) not null,
14 | "path" varchar(255) not null,
15 | "enable" boolean not null,
16 | constraint t_core_pk primary key ("name", "mc_version", "core_version")
17 | );
18 | create table if not exists t_account (
19 | "name" varchar(20) primary key,
20 | "authorization_string" varchar(255) not null,
21 | "permission" varchar(20) not null,
22 | "last_login" timestamp
23 | );
24 |
--------------------------------------------------------------------------------