]> source.dussan.org Git - gitea.git/commitdiff
Add RPM registry (#23380)
authorKN4CK3R <admin@oldschoolhack.me>
Fri, 5 May 2023 20:33:37 +0000 (22:33 +0200)
committerGitHub <noreply@github.com>
Fri, 5 May 2023 20:33:37 +0000 (20:33 +0000)
Fixes #20751

This PR adds a RPM package registry. You can follow [this
tutorial](https://opensource.com/article/18/9/how-build-rpm-packages) to
build a *.rpm package for testing.

This functionality is similar to the Debian registry (#22854) and
therefore shares some methods. I marked this PR as blocked because it
should be merged after #22854.

![grafik](https://user-images.githubusercontent.com/1666336/223806549-d8784fd9-9d79-46a2-9ae2-f038594f636a.png)

29 files changed:
assets/go-licenses.json
custom/conf/app.example.ini
docs/content/doc/administration/config-cheat-sheet.en-us.md
docs/content/doc/usage/packages/overview.en-us.md
docs/content/doc/usage/packages/rpm.en-us.md [new file with mode: 0644]
go.mod
go.sum
models/packages/descriptor.go
models/packages/package.go
models/packages/package_file.go
modules/packages/rpm/metadata.go [new file with mode: 0644]
modules/packages/rpm/metadata_test.go [new file with mode: 0644]
modules/setting/packages.go
options/locale/locale_en-US.ini
public/img/svg/gitea-rpm.svg [new file with mode: 0644]
routers/api/packages/api.go
routers/api/packages/nuget/nuget.go
routers/api/packages/rpm/rpm.go [new file with mode: 0644]
routers/api/v1/packages/package.go
services/forms/package_form.go
services/packages/debian/repository.go
services/packages/packages.go
services/packages/rpm/repository.go [new file with mode: 0644]
templates/package/content/rpm.tmpl [new file with mode: 0644]
templates/package/metadata/rpm.tmpl [new file with mode: 0644]
templates/package/view.tmpl
templates/swagger/v1_json.tmpl
tests/integration/api_packages_rpm_test.go [new file with mode: 0644]
web_src/svg/gitea-rpm.svg [new file with mode: 0644]

index 9bd88b299ec4a8e47a76858f84f7aa88452af06e..49f18e7de70ae7dc766ad60529a608ebe0bec39f 100644 (file)
     "path": "github.com/ClickHouse/clickhouse-go/v2/LICENSE",
     "licenseText": "Copyright 2016-2023 ClickHouse, Inc.\n\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright 2016-2023 ClickHouse, Inc.\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
   },
+  {
+    "name": "github.com/DataDog/zstd",
+    "path": "github.com/DataDog/zstd/LICENSE",
+    "licenseText": "Simplified BSD License\n\nCopyright (c) 2016, Datadog \u003cinfo@datadoghq.com\u003e\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n    * Redistributions of source code must retain the above copyright notice,\n      this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above copyright notice,\n      this list of conditions and the following disclaimer in the documentation\n      and/or other materials provided with the distribution.\n    * Neither the name of the copyright holder nor the names of its contributors\n      may be used to endorse or promote products derived from this software\n      without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+  },
   {
     "name": "github.com/NYTimes/gziphandler",
     "path": "github.com/NYTimes/gziphandler/LICENSE",
     "path": "github.com/santhosh-tekuri/jsonschema/v5/LICENSE",
     "licenseText": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability."
   },
+  {
+    "name": "github.com/sassoftware/go-rpmutils",
+    "path": "github.com/sassoftware/go-rpmutils/LICENSE",
+    "licenseText": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
+  },
   {
     "name": "github.com/segmentio/asm",
     "path": "github.com/segmentio/asm/LICENSE",
index be043f333924a802435e22ad2772a6c16e9a6836..108d940899d08e51bf6793464b0a29673b24224a 100644 (file)
@@ -2512,6 +2512,8 @@ ROUTER = console
 ;LIMIT_SIZE_PUB = -1
 ;; Maximum size of a PyPI upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
 ;LIMIT_SIZE_PYPI = -1
+;; Maximum size of a RPM upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+;LIMIT_SIZE_RPM = -1
 ;; Maximum size of a RubyGems upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
 ;LIMIT_SIZE_RUBYGEMS = -1
 ;; Maximum size of a Swift upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
index b1e393b6c5319b76024d291249d4c01a73663b07..727d011cf7f79fa7b3b5f78b56310128b9976e37 100644 (file)
@@ -1259,6 +1259,7 @@ Task queue configuration has been moved to `queue.task`. However, the below conf
 - `LIMIT_SIZE_NUGET`: **-1**: Maximum size of a NuGet upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
 - `LIMIT_SIZE_PUB`: **-1**: Maximum size of a Pub upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
 - `LIMIT_SIZE_PYPI`: **-1**: Maximum size of a PyPI upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+- `LIMIT_SIZE_RPM`: **-1**: Maximum size of a RPM upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
 - `LIMIT_SIZE_RUBYGEMS`: **-1**: Maximum size of a RubyGems upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
 - `LIMIT_SIZE_SWIFT`: **-1**: Maximum size of a Swift upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
 - `LIMIT_SIZE_VAGRANT`: **-1**: Maximum size of a Vagrant upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
index 8e4fd87e7b7e74330ed5c1e66a8db9655d8d5c3f..8a70a352eb3aa9254435db8e9fb5eb3ff92256a7 100644 (file)
@@ -41,6 +41,7 @@ The following package managers are currently supported:
 | [NuGet]({{< relref "doc/usage/packages/nuget.en-us.md" >}}) | .NET | `nuget` |
 | [Pub]({{< relref "doc/usage/packages/pub.en-us.md" >}}) | Dart | `dart`, `flutter` |
 | [PyPI]({{< relref "doc/usage/packages/pypi.en-us.md" >}}) | Python | `pip`, `twine` |
+| [RPM]({{< relref "doc/usage/packages/rpm.en-us.md" >}}) | - | `yum`, `dnf` |
 | [RubyGems]({{< relref "doc/usage/packages/rubygems.en-us.md" >}}) | Ruby | `gem`, `Bundler` |
 | [Swift]({{< relref "doc/usage/packages/rubygems.en-us.md" >}}) | Swift | `swift` |
 | [Vagrant]({{< relref "doc/usage/packages/vagrant.en-us.md" >}}) | - | `vagrant` |
diff --git a/docs/content/doc/usage/packages/rpm.en-us.md b/docs/content/doc/usage/packages/rpm.en-us.md
new file mode 100644 (file)
index 0000000..c916014
--- /dev/null
@@ -0,0 +1,118 @@
+---
+date: "2023-03-08T00:00:00+00:00"
+title: "RPM Packages Repository"
+slug: "packages/rpm"
+draft: false
+toc: false
+menu:
+  sidebar:
+    parent: "packages"
+    name: "RPM"
+    weight: 105
+    identifier: "rpm"
+---
+
+# RPM Packages Repository
+
+Publish [RPM](https://rpm.org/) packages for your user or organization.
+
+**Table of Contents**
+
+{{< toc >}}
+
+## Requirements
+
+To work with the RPM registry, you need to use a package manager like `yum` or `dnf` to consume packages.
+
+The following examples use `dnf`.
+
+## Configuring the package registry
+
+To register the RPM registry add the url to the list of known apt sources:
+
+```shell
+dnf config-manager --add-repo https://gitea.example.com/api/packages/{owner}/rpm.repo
+```
+
+| Placeholder | Description |
+| ----------- | ----------- |
+| `owner`     | The owner of the package. |
+
+If the registry is private, provide credentials in the url. You can use a password or a [personal access token]({{< relref "doc/developers/api-usage.en-us.md#authentication" >}}):
+
+```shell
+dnf config-manager --add-repo https://{username}:{your_password_or_token}@gitea.example.com/api/packages/{owner}/rpm.repo
+```
+
+You have to add the credentials to the urls in the `rpm.repo` file in `/etc/yum.repos.d` too.
+
+## Publish a package
+
+To publish a RPM package (`*.rpm`), perform a HTTP PUT operation with the package content in the request body.
+
+```
+PUT https://gitea.example.com/api/packages/{owner}/rpm/upload
+```
+
+| Parameter | Description |
+| --------- | ----------- |
+| `owner`   | The owner of the package. |
+
+Example request using HTTP Basic authentication:
+
+```shell
+curl --user your_username:your_password_or_token \
+     --upload-file path/to/file.rpm \
+     https://gitea.example.com/api/packages/testuser/rpm/upload
+```
+
+If you are using 2FA or OAuth use a [personal access token]({{< relref "doc/developers/api-usage.en-us.md#authentication" >}}) instead of the password.
+You cannot publish a file with the same name twice to a package. You must delete the existing package version first.
+
+The server reponds with the following HTTP Status codes.
+
+| HTTP Status Code  | Meaning |
+| ----------------- | ------- |
+| `201 Created`     | The package has been published. |
+| `400 Bad Request` | The package is invalid. |
+| `409 Conflict`    | A package file with the same combination of parameters exist already in the package. |
+
+## Delete a package
+
+To delete a Debian package perform a HTTP DELETE operation. This will delete the package version too if there is no file left.
+
+```
+DELETE https://gitea.example.com/api/packages/{owner}/rpm/{package_name}/{package_version}/{architecture}
+```
+
+| Parameter         | Description |
+| ----------------- | ----------- |
+| `owner`           | The owner of the package. |
+| `package_name`    | The package name. |
+| `package_version` | The package version. |
+| `architecture`    | The package architecture. |
+
+Example request using HTTP Basic authentication:
+
+```shell
+curl --user your_username:your_token_or_password -X DELETE \
+     https://gitea.example.com/api/packages/testuser/rpm/test-package/1.0.0/x86_64
+```
+
+The server reponds with the following HTTP Status codes.
+
+| HTTP Status Code  | Meaning |
+| ----------------- | ------- |
+| `204 No Content`  | Success |
+| `404 Not Found`   | The package or file was not found. |
+
+## Install a package
+
+To install a package from the RPM registry, execute the following commands:
+
+```shell
+# use latest version
+dnf install {package_name}
+# use specific version
+dnf install {package_name}-{package_version}.{architecture}
+```
diff --git a/go.mod b/go.mod
index 176a0c390428d43a6c7c35dd85af32800447b403..24765df1813445235e6304f1fe7d4af45dcc12ab 100644 (file)
--- a/go.mod
+++ b/go.mod
@@ -93,6 +93,7 @@ require (
        github.com/quasoft/websspi v1.1.2
        github.com/redis/go-redis/v9 v9.0.4
        github.com/santhosh-tekuri/jsonschema/v5 v5.3.0
+       github.com/sassoftware/go-rpmutils v0.2.0
        github.com/sergi/go-diff v1.3.1
        github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546
        github.com/stretchr/testify v1.8.2
@@ -130,6 +131,7 @@ require (
        git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 // indirect
        github.com/ClickHouse/ch-go v0.55.0 // indirect
        github.com/ClickHouse/clickhouse-go/v2 v2.9.1 // indirect
+       github.com/DataDog/zstd v1.4.5 // indirect
        github.com/Masterminds/goutils v1.1.1 // indirect
        github.com/Masterminds/semver/v3 v3.2.0 // indirect
        github.com/Masterminds/sprig/v3 v3.2.3 // indirect
diff --git a/go.sum b/go.sum
index 14a3fb7d7b6b036658ca6fc22cfe19bc0e7bbfc0..62c01c690af4773627a2358c8a7fe92ae814eca2 100644 (file)
--- a/go.sum
+++ b/go.sum
@@ -87,6 +87,8 @@ github.com/ClickHouse/ch-go v0.55.0 h1:jw4Tpx887YXrkyL5DfgUome/po8MLz92nz2heOQ6R
 github.com/ClickHouse/ch-go v0.55.0/go.mod h1:kQT2f+yp2p+sagQA/7kS6G3ukym+GQ5KAu1kuFAFDiU=
 github.com/ClickHouse/clickhouse-go/v2 v2.9.1 h1:IeE2bwVvAba7Yw5ZKu98bKI4NpDmykEy6jUaQdJJCk8=
 github.com/ClickHouse/clickhouse-go/v2 v2.9.1/go.mod h1:teXfZNM90iQ99Jnuht+dxQXCuhDZ8nvvMoTJOFrcmcg=
+github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
+github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
 github.com/Julusian/godocdown v0.0.0-20170816220326-6d19f8ff2df8/go.mod h1:INZr5t32rG59/5xeltqoCJoNY7e5x/3xoY9WSWVWg74=
 github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
 github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
@@ -775,6 +777,7 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
 github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
+github.com/klauspost/compress v1.11.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
 github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
 github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
 github.com/klauspost/compress v1.15.6/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
@@ -1081,6 +1084,8 @@ github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb
 github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
 github.com/santhosh-tekuri/jsonschema/v5 v5.3.0 h1:uIkTLo0AGRc8l7h5l9r+GcYi9qfVPt6lD4/bhmzfiKo=
 github.com/santhosh-tekuri/jsonschema/v5 v5.3.0/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H/cDPdvJ/SZJQLWWXWGrZ0=
+github.com/sassoftware/go-rpmutils v0.2.0 h1:pKW0HDYMFWQ5b4JQPiI3WI12hGsVoW0V8+GMoZiI/JE=
+github.com/sassoftware/go-rpmutils v0.2.0/go.mod h1:TJJQYtLe/BeEmEjelI3b7xNZjzAukEkeWKmoakvaOoI=
 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
 github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
 github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
@@ -1269,6 +1274,7 @@ go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
 go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
 go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
 go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
+go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
 go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
 go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
 go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
@@ -1300,6 +1306,7 @@ golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPh
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
+golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
 golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
 golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
 golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
@@ -1583,6 +1590,7 @@ golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtn
 golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
index 9256dd563022eac4d5a269ce8972fd18a34a15cb..1cac2eb02210119e0181f682bdd07e14ddfce619 100644 (file)
@@ -25,6 +25,7 @@ import (
        "code.gitea.io/gitea/modules/packages/nuget"
        "code.gitea.io/gitea/modules/packages/pub"
        "code.gitea.io/gitea/modules/packages/pypi"
+       "code.gitea.io/gitea/modules/packages/rpm"
        "code.gitea.io/gitea/modules/packages/rubygems"
        "code.gitea.io/gitea/modules/packages/swift"
        "code.gitea.io/gitea/modules/packages/vagrant"
@@ -163,6 +164,8 @@ func GetPackageDescriptor(ctx context.Context, pv *PackageVersion) (*PackageDesc
                metadata = &pub.Metadata{}
        case TypePyPI:
                metadata = &pypi.Metadata{}
+       case TypeRpm:
+               metadata = &rpm.VersionMetadata{}
        case TypeRubyGems:
                metadata = &rubygems.Metadata{}
        case TypeSwift:
index cc860b9a907cf2ce9470a06f85533b8f79de6f99..a817ab6ff195cb88c47c45a98d57d093439f6fd2 100644 (file)
@@ -44,6 +44,7 @@ const (
        TypeNuGet     Type = "nuget"
        TypePub       Type = "pub"
        TypePyPI      Type = "pypi"
+       TypeRpm       Type = "rpm"
        TypeRubyGems  Type = "rubygems"
        TypeSwift     Type = "swift"
        TypeVagrant   Type = "vagrant"
@@ -64,6 +65,7 @@ var TypeList = []Type{
        TypeNuGet,
        TypePub,
        TypePyPI,
+       TypeRpm,
        TypeRubyGems,
        TypeSwift,
        TypeVagrant,
@@ -100,6 +102,8 @@ func (pt Type) Name() string {
                return "Pub"
        case TypePyPI:
                return "PyPI"
+       case TypeRpm:
+               return "RPM"
        case TypeRubyGems:
                return "RubyGems"
        case TypeSwift:
@@ -141,6 +145,8 @@ func (pt Type) SVGName() string {
                return "gitea-pub"
        case TypePyPI:
                return "gitea-python"
+       case TypeRpm:
+               return "gitea-rpm"
        case TypeRubyGems:
                return "gitea-rubygems"
        case TypeSwift:
index 28e2a0111a5e4ba409223dda162b0ed1930bbc4a..1c2c9ac072f417ad5868148b1eb4ad19e02271dc 100644 (file)
@@ -118,7 +118,7 @@ func DeleteFileByID(ctx context.Context, fileID int64) error {
 // PackageFileSearchOptions are options for SearchXXX methods
 type PackageFileSearchOptions struct {
        OwnerID       int64
-       PackageType   string
+       PackageType   Type
        VersionID     int64
        Query         string
        CompositeKey  string
diff --git a/modules/packages/rpm/metadata.go b/modules/packages/rpm/metadata.go
new file mode 100644 (file)
index 0000000..607ea42
--- /dev/null
@@ -0,0 +1,296 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package rpm
+
+import (
+       "fmt"
+       "io"
+       "strings"
+
+       "code.gitea.io/gitea/modules/timeutil"
+       "code.gitea.io/gitea/modules/validation"
+
+       "github.com/sassoftware/go-rpmutils"
+)
+
+const (
+       PropertyMetadata = "rpm.metdata"
+
+       SettingKeyPrivate = "rpm.key.private"
+       SettingKeyPublic  = "rpm.key.public"
+
+       RepositoryPackage = "_rpm"
+       RepositoryVersion = "_repository"
+)
+
+const (
+       // Can't use the syscall constants because they are not available for windows build.
+       sIFMT  = 0xf000
+       sIFDIR = 0x4000
+       sIXUSR = 0x40
+       sIXGRP = 0x8
+       sIXOTH = 0x1
+)
+
+// https://rpm-software-management.github.io/rpm/manual/spec.html
+// https://refspecs.linuxbase.org/LSB_3.1.0/LSB-Core-generic/LSB-Core-generic/pkgformat.html
+
+type Package struct {
+       Name            string
+       Version         string
+       VersionMetadata *VersionMetadata
+       FileMetadata    *FileMetadata
+}
+
+type VersionMetadata struct {
+       License     string `json:"license,omitempty"`
+       ProjectURL  string `json:"project_url,omitempty"`
+       Summary     string `json:"summary,omitempty"`
+       Description string `json:"description,omitempty"`
+}
+
+type FileMetadata struct {
+       Architecture  string `json:"architecture,omitempty"`
+       Epoch         string `json:"epoch,omitempty"`
+       Version       string `json:"version,omitempty"`
+       Release       string `json:"release,omitempty"`
+       Vendor        string `json:"vendor,omitempty"`
+       Group         string `json:"group,omitempty"`
+       Packager      string `json:"packager,omitempty"`
+       SourceRpm     string `json:"source_rpm,omitempty"`
+       BuildHost     string `json:"build_host,omitempty"`
+       BuildTime     uint64 `json:"build_time,omitempty"`
+       FileTime      uint64 `json:"file_time,omitempty"`
+       InstalledSize uint64 `json:"installed_size,omitempty"`
+       ArchiveSize   uint64 `json:"archive_size,omitempty"`
+
+       Provides  []*Entry `json:"provide,omitempty"`
+       Requires  []*Entry `json:"require,omitempty"`
+       Conflicts []*Entry `json:"conflict,omitempty"`
+       Obsoletes []*Entry `json:"obsolete,omitempty"`
+
+       Files []*File `json:"files,omitempty"`
+
+       Changelogs []*Changelog `json:"changelogs,omitempty"`
+}
+
+type Entry struct {
+       Name    string `json:"name" xml:"name,attr"`
+       Flags   string `json:"flags,omitempty" xml:"flags,attr,omitempty"`
+       Version string `json:"version,omitempty" xml:"ver,attr,omitempty"`
+       Epoch   string `json:"epoch,omitempty" xml:"epoch,attr,omitempty"`
+       Release string `json:"release,omitempty" xml:"rel,attr,omitempty"`
+}
+
+type File struct {
+       Path         string `json:"path" xml:",chardata"`
+       Type         string `json:"type,omitempty" xml:"type,attr,omitempty"`
+       IsExecutable bool   `json:"is_executable" xml:"-"`
+}
+
+type Changelog struct {
+       Author string             `json:"author,omitempty" xml:"author,attr"`
+       Date   timeutil.TimeStamp `json:"date,omitempty" xml:"date,attr"`
+       Text   string             `json:"text,omitempty" xml:",chardata"`
+}
+
+// ParsePackage parses the RPM package file
+func ParsePackage(r io.Reader) (*Package, error) {
+       rpm, err := rpmutils.ReadRpm(r)
+       if err != nil {
+               return nil, err
+       }
+
+       nevra, err := rpm.Header.GetNEVRA()
+       if err != nil {
+               return nil, err
+       }
+
+       version := fmt.Sprintf("%s-%s", nevra.Version, nevra.Release)
+       if nevra.Epoch != "" && nevra.Epoch != "0" {
+               version = fmt.Sprintf("%s-%s", nevra.Epoch, version)
+       }
+
+       p := &Package{
+               Name:    nevra.Name,
+               Version: version,
+               VersionMetadata: &VersionMetadata{
+                       Summary:     getString(rpm.Header, rpmutils.SUMMARY),
+                       Description: getString(rpm.Header, rpmutils.DESCRIPTION),
+                       License:     getString(rpm.Header, rpmutils.LICENSE),
+                       ProjectURL:  getString(rpm.Header, rpmutils.URL),
+               },
+               FileMetadata: &FileMetadata{
+                       Architecture:  nevra.Arch,
+                       Epoch:         nevra.Epoch,
+                       Version:       nevra.Version,
+                       Release:       nevra.Release,
+                       Vendor:        getString(rpm.Header, rpmutils.VENDOR),
+                       Group:         getString(rpm.Header, rpmutils.GROUP),
+                       Packager:      getString(rpm.Header, rpmutils.PACKAGER),
+                       SourceRpm:     getString(rpm.Header, rpmutils.SOURCERPM),
+                       BuildHost:     getString(rpm.Header, rpmutils.BUILDHOST),
+                       BuildTime:     getUInt64(rpm.Header, rpmutils.BUILDTIME),
+                       FileTime:      getUInt64(rpm.Header, rpmutils.FILEMTIMES),
+                       InstalledSize: getUInt64(rpm.Header, rpmutils.SIZE),
+                       ArchiveSize:   getUInt64(rpm.Header, rpmutils.SIG_PAYLOADSIZE),
+
+                       Provides:   getEntries(rpm.Header, rpmutils.PROVIDENAME, rpmutils.PROVIDEVERSION, rpmutils.PROVIDEFLAGS),
+                       Requires:   getEntries(rpm.Header, rpmutils.REQUIRENAME, rpmutils.REQUIREVERSION, rpmutils.REQUIREFLAGS),
+                       Conflicts:  getEntries(rpm.Header, 1054 /*rpmutils.CONFLICTNAME*/, 1055 /*rpmutils.CONFLICTVERSION*/, 1053 /*rpmutils.CONFLICTFLAGS*/), // https://github.com/sassoftware/go-rpmutils/pull/24
+                       Obsoletes:  getEntries(rpm.Header, rpmutils.OBSOLETENAME, rpmutils.OBSOLETEVERSION, rpmutils.OBSOLETEFLAGS),
+                       Files:      getFiles(rpm.Header),
+                       Changelogs: getChangelogs(rpm.Header),
+               },
+       }
+
+       if !validation.IsValidURL(p.VersionMetadata.ProjectURL) {
+               p.VersionMetadata.ProjectURL = ""
+       }
+
+       return p, nil
+}
+
+func getString(h *rpmutils.RpmHeader, tag int) string {
+       values, err := h.GetStrings(tag)
+       if err != nil || len(values) < 1 {
+               return ""
+       }
+       return values[0]
+}
+
+func getUInt64(h *rpmutils.RpmHeader, tag int) uint64 {
+       values, err := h.GetUint64s(tag)
+       if err != nil || len(values) < 1 {
+               return 0
+       }
+       return values[0]
+}
+
+func getEntries(h *rpmutils.RpmHeader, namesTag, versionsTag, flagsTag int) []*Entry {
+       names, err := h.GetStrings(namesTag)
+       if err != nil || len(names) == 0 {
+               return nil
+       }
+       flags, err := h.GetUint64s(flagsTag)
+       if err != nil || len(flags) == 0 {
+               return nil
+       }
+       versions, err := h.GetStrings(versionsTag)
+       if err != nil || len(versions) == 0 {
+               return nil
+       }
+       if len(names) != len(flags) || len(names) != len(versions) {
+               return nil
+       }
+
+       entries := make([]*Entry, 0, len(names))
+       for i := range names {
+               e := &Entry{
+                       Name: names[i],
+               }
+
+               flags := flags[i]
+               if (flags&rpmutils.RPMSENSE_GREATER) != 0 && (flags&rpmutils.RPMSENSE_EQUAL) != 0 {
+                       e.Flags = "GE"
+               } else if (flags&rpmutils.RPMSENSE_LESS) != 0 && (flags&rpmutils.RPMSENSE_EQUAL) != 0 {
+                       e.Flags = "LE"
+               } else if (flags & rpmutils.RPMSENSE_GREATER) != 0 {
+                       e.Flags = "GT"
+               } else if (flags & rpmutils.RPMSENSE_LESS) != 0 {
+                       e.Flags = "LT"
+               } else if (flags & rpmutils.RPMSENSE_EQUAL) != 0 {
+                       e.Flags = "EQ"
+               }
+
+               version := versions[i]
+               if version != "" {
+                       parts := strings.Split(version, "-")
+
+                       versionParts := strings.Split(parts[0], ":")
+                       if len(versionParts) == 2 {
+                               e.Version = versionParts[1]
+                               e.Epoch = versionParts[0]
+                       } else {
+                               e.Version = versionParts[0]
+                               e.Epoch = "0"
+                       }
+
+                       if len(parts) > 1 {
+                               e.Release = parts[1]
+                       }
+               }
+
+               entries = append(entries, e)
+       }
+       return entries
+}
+
+func getFiles(h *rpmutils.RpmHeader) []*File {
+       baseNames, _ := h.GetStrings(rpmutils.BASENAMES)
+       dirNames, _ := h.GetStrings(rpmutils.DIRNAMES)
+       dirIndexes, _ := h.GetUint32s(rpmutils.DIRINDEXES)
+       fileFlags, _ := h.GetUint32s(rpmutils.FILEFLAGS)
+       fileModes, _ := h.GetUint32s(rpmutils.FILEMODES)
+
+       files := make([]*File, 0, len(baseNames))
+       for i := range baseNames {
+               if len(dirIndexes) <= i {
+                       continue
+               }
+               dirIndex := dirIndexes[i]
+               if len(dirNames) <= int(dirIndex) {
+                       continue
+               }
+
+               var fileType string
+               var isExecutable bool
+               if i < len(fileFlags) && (fileFlags[i]&rpmutils.RPMFILE_GHOST) != 0 {
+                       fileType = "ghost"
+               } else if i < len(fileModes) {
+                       if (fileModes[i] & sIFMT) == sIFDIR {
+                               fileType = "dir"
+                       } else {
+                               mode := fileModes[i] & ^uint32(sIFMT)
+                               isExecutable = (mode&sIXUSR) != 0 || (mode&sIXGRP) != 0 || (mode&sIXOTH) != 0
+                       }
+               }
+
+               files = append(files, &File{
+                       Path:         dirNames[dirIndex] + baseNames[i],
+                       Type:         fileType,
+                       IsExecutable: isExecutable,
+               })
+       }
+
+       return files
+}
+
+func getChangelogs(h *rpmutils.RpmHeader) []*Changelog {
+       texts, err := h.GetStrings(rpmutils.CHANGELOGTEXT)
+       if err != nil || len(texts) == 0 {
+               return nil
+       }
+       authors, err := h.GetStrings(rpmutils.CHANGELOGNAME)
+       if err != nil || len(authors) == 0 {
+               return nil
+       }
+       times, err := h.GetUint32s(rpmutils.CHANGELOGTIME)
+       if err != nil || len(times) == 0 {
+               return nil
+       }
+       if len(texts) != len(authors) || len(texts) != len(times) {
+               return nil
+       }
+
+       changelogs := make([]*Changelog, 0, len(texts))
+       for i := range texts {
+               changelogs = append(changelogs, &Changelog{
+                       Author: authors[i],
+                       Date:   timeutil.TimeStamp(times[i]),
+                       Text:   texts[i],
+               })
+       }
+       return changelogs
+}
diff --git a/modules/packages/rpm/metadata_test.go b/modules/packages/rpm/metadata_test.go
new file mode 100644 (file)
index 0000000..bb538ef
--- /dev/null
@@ -0,0 +1,163 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package rpm
+
+import (
+       "bytes"
+       "compress/gzip"
+       "encoding/base64"
+       "testing"
+
+       "github.com/stretchr/testify/assert"
+)
+
+func TestParsePackage(t *testing.T) {
+       base64RpmPackageContent := `H4sICFayB2QCAGdpdGVhLXRlc3QtMS4wLjItMS14ODZfNjQucnBtAO2YV4gTQRjHJzl7wbNhhxVF
+VNwk2zd2PdvZ9Sxnd3Z3NllNsmF3o6congVFsWFHRWwIImIXfRER0QcRfPBJEXvvBQvWSfZTT0VQ
+8TF/MuU33zcz3+zOJGEe73lyuQBRBWKWRzDrEddjuVAkxLMc+lsFUOWfm5bvvReAalWECg/TsivU
+dyKa0U61aVnl6wj0Uxe4nc8F92hZiaYE8CO/P0r7/Quegr0c7M/AvoCaGZEIWNGUqMHrhhGROIUT
+Zc7gOAOraoQzCNZ0WdU0HpEI5jiB4zlek3gT85wqCBomhomxoGCs8wImWMImbxqKgXVNUKKaqShR
+STKVKK9glFUNcf2g+/t27xs16v5x/eyOKftVGlIhyiuvvPLKK6+88sorr7zyyiuvvPKCO5HPnz+v
+pGVhhXsTsFVeSstuWR9anwU+Bk3Vch5wTwL3JkHg+8C1gR8A169wj1KdpobAj4HbAT+Be5VewE+h
+fz/g52AvBX4N9vHAb4AnA7+F8ePAH8BuA38ELgf+BLzQ50oIeBlw0OdAOXAlP57AGuCsbwGtbgCu
+DrwRuAb4bwau6T/PwFbgWsDXgWuD/y3gOmC/B1wI/Bi4AcT3Arih3z9YCNzI9w9m/YKUG4Nd9N9z
+pSZgHwrcFPgccFt//OADGE+F/q+Ao+D/FrijzwV1gbv4/QvaAHcFDgF3B5aB+wB3Be7rz1dQCtwP
+eDxwMcw3GbgU7AasdwzYE8DjwT4L/CeAvRx4IvBCYA3iWQds+FzpDjABfghsAj8BTgA/A/b8+StX
+A84A1wKe5s9fuRB4JpzHZv55rL8a/Dv49vpn/PErR4BvQX8Z+Db4l2W5CH2/f0W5+1fEoeFDBzFp
+rE/FMcK4mWQSOzN+aDOIqztW2rPsFKIyqh7sQERR42RVMSKihnzVHlQ8Ag0YLBYNEIajkhmuR5Io
+7nlpt2M4nJs0ZNkoYaUyZahMlSfJImr1n1WjFVNCPCaTZgYNGdGL8YN2mX8WHfA/C7ViHJK0pxHG
+SrkeTiSI4T+7ubf85yrzRCQRQ5EVxVAjvIBVRY/KRFAVReIkhfARSddNSceayQkGliIKb0q8RAxJ
+5QWNVxHIsW3Pz369bw+5jh5y0klE9Znqm0dF57b0HbGy2A5lVUBTZZrqZjdUjYoprFmpsBtHP5d0
++ISltS2yk2mHuC4x+lgJMhgnidvuqy3b0suK0bm+tw3FMxI2zjm7/fA0MtQhplX2s7nYLZ2ZC0yg
+CxJZDokhORTJlrlcCvG5OieGBERlVCs7CfuS6WzQ/T2j+9f92BWxTFEcp2IkYccYGp2LYySEfreq
+irue4WRF5XkpKovw2wgpq2rZBI8bQZkzxEkiYaNwxnXCCVvHidzIiB3CM2yMYdNWmjDsaLovaE4c
+x3a6mLaTxB7rEj3jWN4M2p7uwPaa1GfI8BHFfcZMKhkycnhR7y781/a+A4t7FpWWTupRUtKbegwZ
+XMKwJinTSe70uhRcj55qNu3YHtE922Fdz7FTMTq9Q3TbMdiYrrPudMvT44S6u2miu138eC0tTN9D
+2CFGHHtQsHHsGCRFDFbXuT9wx6mUTZfseydlkWZeJkW6xOgYjqXT+LA7I6XHaUx2xmUzqelWymA9
+rCXI9+D1BHbjsITssqhBNysw0tOWjcpmIh6+aViYPfftw8ZSGfRVPUqKiosZj5R5qGmk/8AjjRbZ
+d8b3vvngdPHx3HvMeCarIk7VVSwbgoZVkceEVyOmyUmGxBGNYDVKSFSOGlIkGqWnUZFkiY/wsmhK
+Mu0UFYgZ/bYnuvn/vz4wtCz8qMwsHUvP0PX3tbYFUctAPdrY6tiiDtcCddDECahx7SuVNP5dpmb5
+9tMDyaXb7OAlk5acuPn57ss9mw6Wym0m1Fq2cej7tUt2LL4/b8enXU2fndk+fvv57ndnt55/cQob
+7tpp/pEjDS7cGPZ6BY430+7danDq6f42Nw49b9F7zp6BiKpJb9s5P0AYN2+L159cnrur636rx+v1
+7ae1K28QbMMcqI8CqwIrgwg9nTOp8Oj9q81plUY7ZuwXN8Vvs8wbAAA=`
+       rpmPackageContent, err := base64.StdEncoding.DecodeString(base64RpmPackageContent)
+       assert.NoError(t, err)
+
+       zr, err := gzip.NewReader(bytes.NewReader(rpmPackageContent))
+       assert.NoError(t, err)
+
+       p, err := ParsePackage(zr)
+       assert.NotNil(t, p)
+       assert.NoError(t, err)
+
+       assert.Equal(t, "gitea-test", p.Name)
+       assert.Equal(t, "1.0.2-1", p.Version)
+       assert.NotNil(t, p.VersionMetadata)
+       assert.NotNil(t, p.FileMetadata)
+
+       assert.Equal(t, "MIT", p.VersionMetadata.License)
+       assert.Equal(t, "https://gitea.io", p.VersionMetadata.ProjectURL)
+       assert.Equal(t, "RPM package summary", p.VersionMetadata.Summary)
+       assert.Equal(t, "RPM package description", p.VersionMetadata.Description)
+
+       assert.Equal(t, "x86_64", p.FileMetadata.Architecture)
+       assert.Equal(t, "0", p.FileMetadata.Epoch)
+       assert.Equal(t, "1.0.2", p.FileMetadata.Version)
+       assert.Equal(t, "1", p.FileMetadata.Release)
+       assert.Empty(t, p.FileMetadata.Vendor)
+       assert.Equal(t, "KN4CK3R", p.FileMetadata.Packager)
+       assert.Equal(t, "gitea-test-1.0.2-1.src.rpm", p.FileMetadata.SourceRpm)
+       assert.Equal(t, "e44b1687d04b", p.FileMetadata.BuildHost)
+       assert.EqualValues(t, 1678225964, p.FileMetadata.BuildTime)
+       assert.EqualValues(t, 1678225964, p.FileMetadata.FileTime)
+       assert.EqualValues(t, 13, p.FileMetadata.InstalledSize)
+       assert.EqualValues(t, 272, p.FileMetadata.ArchiveSize)
+       assert.Empty(t, p.FileMetadata.Conflicts)
+       assert.Empty(t, p.FileMetadata.Obsoletes)
+
+       assert.ElementsMatch(
+               t,
+               []*Entry{
+                       {
+                               Name:    "gitea-test",
+                               Flags:   "EQ",
+                               Version: "1.0.2",
+                               Epoch:   "0",
+                               Release: "1",
+                       },
+                       {
+                               Name:    "gitea-test(x86-64)",
+                               Flags:   "EQ",
+                               Version: "1.0.2",
+                               Epoch:   "0",
+                               Release: "1",
+                       },
+               },
+               p.FileMetadata.Provides,
+       )
+       assert.ElementsMatch(
+               t,
+               []*Entry{
+                       {
+                               Name: "/bin/sh",
+                       },
+                       {
+                               Name: "/bin/sh",
+                       },
+                       {
+                               Name: "/bin/sh",
+                       },
+                       {
+                               Name:    "rpmlib(CompressedFileNames)",
+                               Flags:   "LE",
+                               Version: "3.0.4",
+                               Epoch:   "0",
+                               Release: "1",
+                       },
+                       {
+                               Name:    "rpmlib(FileDigests)",
+                               Flags:   "LE",
+                               Version: "4.6.0",
+                               Epoch:   "0",
+                               Release: "1",
+                       },
+                       {
+                               Name:    "rpmlib(PayloadFilesHavePrefix)",
+                               Flags:   "LE",
+                               Version: "4.0",
+                               Epoch:   "0",
+                               Release: "1",
+                       },
+                       {
+                               Name:    "rpmlib(PayloadIsXz)",
+                               Flags:   "LE",
+                               Version: "5.2",
+                               Epoch:   "0",
+                               Release: "1",
+                       },
+               },
+               p.FileMetadata.Requires,
+       )
+       assert.ElementsMatch(
+               t,
+               []*File{
+                       {
+                               Path:         "/usr/local/bin/hello",
+                               IsExecutable: true,
+                       },
+               },
+               p.FileMetadata.Files,
+       )
+       assert.ElementsMatch(
+               t,
+               []*Changelog{
+                       {
+                               Author: "KN4CK3R <dummy@gitea.io>",
+                               Date:   1678276800,
+                               Text:   "- Changelog message.",
+                       },
+               },
+               p.FileMetadata.Changelogs,
+       )
+}
index b52bbf40c7b9009ef8749ac84ee0f1b8fd54a581..00d8b6122f3bf209e7f40ef7ae23b465ed0e1d86 100644 (file)
@@ -38,6 +38,7 @@ var (
                LimitSizeNuGet       int64
                LimitSizePub         int64
                LimitSizePyPI        int64
+               LimitSizeRpm         int64
                LimitSizeRubyGems    int64
                LimitSizeSwift       int64
                LimitSizeVagrant     int64
@@ -82,6 +83,7 @@ func loadPackagesFrom(rootCfg ConfigProvider) {
        Packages.LimitSizeNuGet = mustBytes(sec, "LIMIT_SIZE_NUGET")
        Packages.LimitSizePub = mustBytes(sec, "LIMIT_SIZE_PUB")
        Packages.LimitSizePyPI = mustBytes(sec, "LIMIT_SIZE_PYPI")
+       Packages.LimitSizeRpm = mustBytes(sec, "LIMIT_SIZE_RPM")
        Packages.LimitSizeRubyGems = mustBytes(sec, "LIMIT_SIZE_RUBYGEMS")
        Packages.LimitSizeSwift = mustBytes(sec, "LIMIT_SIZE_SWIFT")
        Packages.LimitSizeVagrant = mustBytes(sec, "LIMIT_SIZE_VAGRANT")
index fc5eccb2db24194b6ac72dfd0997ddb8cd516d64..f48ebbdc746c88b0f51c2fbb35a19bc69628ee05 100644 (file)
@@ -3308,6 +3308,9 @@ pub.documentation = For more information on the Pub registry, see <a target="_bl
 pypi.requires = Requires Python
 pypi.install = To install the package using pip, run the following command:
 pypi.documentation = For more information on the PyPI registry, see <a target="_blank" rel="noopener noreferrer" href="%s">the documentation</a>.
+rpm.registry = Setup this registry from the command line:
+rpm.install = To install the package, run the following command:
+rpm.documentation = For more information on the RPM registry, see <a target="_blank" rel="noopener noreferrer" href="%s">the documentation</a>.
 rubygems.install = To install the package using gem, run the following command:
 rubygems.install2 = or add it to the Gemfile:
 rubygems.dependencies.runtime = Runtime Dependencies
diff --git a/public/img/svg/gitea-rpm.svg b/public/img/svg/gitea-rpm.svg
new file mode 100644 (file)
index 0000000..d0d3db1
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 409" class="svg gitea-rpm" width="16" height="16" aria-hidden="true"><path fill="#040404" d="M231.303 13.092c13.965-2.147 28.064-3.716 42.185-4.46 2.508-.598 5.104-.476 7.667-.567 20.042-.64 40.107-.61 60.138.292 36.152 2.556 72.126 8.562 107 18.576l-.147.196c-5.113 6.915-10.241 13.82-15.254 20.809-1.502.277-2.818-.673-4.155-1.151-26.464-10.918-54.628-17.254-82.97-20.687-46.628-4.775-94.368-.939-139.097 13.41-32.93 10.652-64.738 27.075-89.302 51.85-17.41 17.798-30.764 40.818-32.586 66.049-1.687 12.021 1.191 24.02 3.738 35.696l-110.456 19.476c-.267-19.487 4.819-38.774 12.91-56.405 16.098-34.33 44.018-61.76 75.115-82.759 21.03-14.12 43.762-25.641 67.382-34.774 31.463-12.299 64.416-20.698 97.834-25.542z"/><path fill="#d72123" d="M432.95 47.933c5.014-6.986 10.14-13.888 15.254-20.81 46.828 13.71 92.046 35.375 128.987 67.638 15.098 13.521 28.764 28.82 39.251 46.229 12.421 20.487 20.298 43.951 21.143 67.982-36.73-6.42-73.437-12.888-110.167-19.331 4.52-14.377 4.558-30.02 1.463-44.696-5.736-25.353-21.987-47.117-41.596-63.638-16.343-13.754-34.985-24.775-54.739-32.841z"/><path d="M297.519 149.145c2.162-3.521 5.968-6.329 10.298-5.808 4.894.126 8.664 4.169 9.677 8.746l345.3 61.416-1.154 5.784-345.188-60.527c-2.753 4.79-9.14 7.265-14.154 4.436-3.478-1.52-4.666-5.244-6.236-8.367l-21.253-3.405.974-5.978z"/><path fill="#040404" d="m135.757 316.795.194-85.936 36.552.068c38.53.072 47.195.626 64.282 4.108 41.896 8.537 69.704 28.62 75.16 54.273 5.53 26.008-15.066 49.761-56.706 65.415-14.976 5.63-35.297 10.045-51.45 11.188l-6.923.488-.415 27.62-2.457.439c-1.352.241-13.932 2.004-27.964 3.917s-26.62 3.674-27.986 3.915l-2.478.438zm66.671 33.152c1.966-.462 7-2.482 11.188-4.49 15.754-7.55 27.253-18.276 33.441-31.208 4.787-9.989 5.143-19.365 1.141-30.063-4.583-12.255-17.676-25.576-32.43-33.02-4.86-2.45-14.532-5.452-17.554-5.452h-1.729v52.539c0 47.05.124 52.54 1.186 52.54.652 0 2.793-.379 4.758-.84zm-224.422-52.073v-67.382h60.916v1.618c0 1.422.3 1.556 2.475 1.109 14.298-2.941 35.852-4.27 45.095-2.778 3.21.518 8.167 1.734 11.017 2.703 6.034 2.052 16.765 6.692 16.754 7.246-.004.21-2.823 2.066-6.268 4.126s-10.793 6.619-16.331 10.13l-10.07 6.382-2.851-1.938c-5.736-3.897-11.877-5.376-22.31-5.376-5.506 0-11.198.433-13.509 1.028l-3.997 1.028v109.489H-21.99zm374.63-.05v-67.438l119.32.333c121.1.337 124.766.423 140.209 3.253 11.566 2.12 18.365 4.687 21.987 8.309l3.236 3.234v119.766h-60.916V247.404l-3.236-1.391c-2.832-1.218-6.019-1.426-25.509-1.666l-22.276-.274v121.21h-60.905l-.39-118.21-3.045-1.173c-2.419-.93-7.634-1.233-25.32-1.47l-22.275-.296v121.21H352.6z"/></svg>
\ No newline at end of file
index ee1feb1414c1cf4ac8325df38e3eb9dab4e3e7da..9b24918f51396eb4d5048d0139af22368264c2f0 100644 (file)
@@ -29,6 +29,7 @@ import (
        "code.gitea.io/gitea/routers/api/packages/nuget"
        "code.gitea.io/gitea/routers/api/packages/pub"
        "code.gitea.io/gitea/routers/api/packages/pypi"
+       "code.gitea.io/gitea/routers/api/packages/rpm"
        "code.gitea.io/gitea/routers/api/packages/rubygems"
        "code.gitea.io/gitea/routers/api/packages/swift"
        "code.gitea.io/gitea/routers/api/packages/vagrant"
@@ -420,6 +421,16 @@ func CommonRoutes(ctx gocontext.Context) *web.Route {
                        r.Get("/files/{id}/{version}/{filename}", pypi.DownloadPackageFile)
                        r.Get("/simple/{id}", pypi.PackageMetadata)
                }, reqPackageAccess(perm.AccessModeRead))
+               r.Group("/rpm", func() {
+                       r.Get(".repo", rpm.GetRepositoryConfig)
+                       r.Get("/repository.key", rpm.GetRepositoryKey)
+                       r.Put("/upload", reqPackageAccess(perm.AccessModeWrite), rpm.UploadPackageFile)
+                       r.Group("/package/{name}/{version}/{architecture}", func() {
+                               r.Get("", rpm.DownloadPackageFile)
+                               r.Delete("", reqPackageAccess(perm.AccessModeWrite), rpm.DeletePackageFile)
+                       })
+                       r.Get("/repodata/{filename}", rpm.GetRepositoryFile)
+               }, reqPackageAccess(perm.AccessModeRead))
                r.Group("/rubygems", func() {
                        r.Get("/specs.4.8.gz", rubygems.EnumeratePackages)
                        r.Get("/latest_specs.4.8.gz", rubygems.EnumeratePackagesLatest)
index f6143ce291d231e909f4f938824eed8fcfbff9fc..716d8a969d665ae9d962cd9eaedeaf2ba116defd 100644 (file)
@@ -585,7 +585,7 @@ func DownloadSymbolFile(ctx *context.Context) {
 
        pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{
                OwnerID:     ctx.Package.Owner.ID,
-               PackageType: string(packages_model.TypeNuGet),
+               PackageType: packages_model.TypeNuGet,
                Query:       filename,
                Properties: map[string]string{
                        nuget_module.PropertySymbolID: strings.ToLower(guid),
diff --git a/routers/api/packages/rpm/rpm.go b/routers/api/packages/rpm/rpm.go
new file mode 100644 (file)
index 0000000..73e4572
--- /dev/null
@@ -0,0 +1,268 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package rpm
+
+import (
+       stdctx "context"
+       "errors"
+       "fmt"
+       "io"
+       "net/http"
+       "strings"
+
+       "code.gitea.io/gitea/models/db"
+       packages_model "code.gitea.io/gitea/models/packages"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/json"
+       "code.gitea.io/gitea/modules/notification"
+       packages_module "code.gitea.io/gitea/modules/packages"
+       rpm_module "code.gitea.io/gitea/modules/packages/rpm"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/util"
+       "code.gitea.io/gitea/routers/api/packages/helper"
+       packages_service "code.gitea.io/gitea/services/packages"
+       rpm_service "code.gitea.io/gitea/services/packages/rpm"
+)
+
+func apiError(ctx *context.Context, status int, obj interface{}) {
+       helper.LogAndProcessError(ctx, status, obj, func(message string) {
+               ctx.PlainText(status, message)
+       })
+}
+
+// https://dnf.readthedocs.io/en/latest/conf_ref.html
+func GetRepositoryConfig(ctx *context.Context) {
+       url := fmt.Sprintf("%sapi/packages/%s/rpm", setting.AppURL, ctx.Package.Owner.Name)
+
+       ctx.PlainText(http.StatusOK, `[gitea-`+ctx.Package.Owner.LowerName+`]
+name=`+ctx.Package.Owner.Name+` - `+setting.AppName+`
+baseurl=`+url+`
+enabled=1
+gpgcheck=1
+gpgkey=`+url+`/repository.key`)
+}
+
+// Gets or creates the PGP public key used to sign repository metadata files
+func GetRepositoryKey(ctx *context.Context) {
+       _, pub, err := rpm_service.GetOrCreateKeyPair(ctx.Package.Owner.ID)
+       if err != nil {
+               apiError(ctx, http.StatusInternalServerError, err)
+               return
+       }
+
+       ctx.ServeContent(strings.NewReader(pub), &context.ServeHeaderOptions{
+               ContentType: "application/pgp-keys",
+               Filename:    "repository.key",
+       })
+}
+
+// Gets a pre-generated repository metadata file
+func GetRepositoryFile(ctx *context.Context) {
+       pv, err := rpm_service.GetOrCreateRepositoryVersion(ctx.Package.Owner.ID)
+       if err != nil {
+               apiError(ctx, http.StatusInternalServerError, err)
+               return
+       }
+
+       s, pf, err := packages_service.GetFileStreamByPackageVersion(
+               ctx,
+               pv,
+               &packages_service.PackageFileInfo{
+                       Filename: ctx.Params("filename"),
+               },
+       )
+       if err != nil {
+               if errors.Is(err, util.ErrNotExist) {
+                       apiError(ctx, http.StatusNotFound, err)
+               } else {
+                       apiError(ctx, http.StatusInternalServerError, err)
+               }
+               return
+       }
+       defer s.Close()
+
+       ctx.ServeContent(s, &context.ServeHeaderOptions{
+               Filename:     pf.Name,
+               LastModified: pf.CreatedUnix.AsLocalTime(),
+       })
+}
+
+func UploadPackageFile(ctx *context.Context) {
+       upload, close, err := ctx.UploadStream()
+       if err != nil {
+               apiError(ctx, http.StatusInternalServerError, err)
+               return
+       }
+       if close {
+               defer upload.Close()
+       }
+
+       buf, err := packages_module.CreateHashedBufferFromReader(upload)
+       if err != nil {
+               apiError(ctx, http.StatusInternalServerError, err)
+               return
+       }
+       defer buf.Close()
+
+       pck, err := rpm_module.ParsePackage(buf)
+       if err != nil {
+               if errors.Is(err, util.ErrInvalidArgument) {
+                       apiError(ctx, http.StatusBadRequest, err)
+               } else {
+                       apiError(ctx, http.StatusInternalServerError, err)
+               }
+               return
+       }
+
+       if _, err := buf.Seek(0, io.SeekStart); err != nil {
+               apiError(ctx, http.StatusInternalServerError, err)
+               return
+       }
+
+       fileMetadataRaw, err := json.Marshal(pck.FileMetadata)
+       if err != nil {
+               apiError(ctx, http.StatusInternalServerError, err)
+               return
+       }
+
+       _, _, err = packages_service.CreatePackageOrAddFileToExisting(
+               &packages_service.PackageCreationInfo{
+                       PackageInfo: packages_service.PackageInfo{
+                               Owner:       ctx.Package.Owner,
+                               PackageType: packages_model.TypeRpm,
+                               Name:        pck.Name,
+                               Version:     pck.Version,
+                       },
+                       Creator:  ctx.Doer,
+                       Metadata: pck.VersionMetadata,
+               },
+               &packages_service.PackageFileCreationInfo{
+                       PackageFileInfo: packages_service.PackageFileInfo{
+                               Filename: fmt.Sprintf("%s-%s.%s.rpm", pck.Name, pck.Version, pck.FileMetadata.Architecture),
+                       },
+                       Creator: ctx.Doer,
+                       Data:    buf,
+                       IsLead:  true,
+                       Properties: map[string]string{
+                               rpm_module.PropertyMetadata: string(fileMetadataRaw),
+                       },
+               },
+       )
+       if err != nil {
+               switch err {
+               case packages_model.ErrDuplicatePackageVersion, packages_model.ErrDuplicatePackageFile:
+                       apiError(ctx, http.StatusConflict, err)
+               case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
+                       apiError(ctx, http.StatusForbidden, err)
+               default:
+                       apiError(ctx, http.StatusInternalServerError, err)
+               }
+               return
+       }
+
+       if err := rpm_service.BuildRepositoryFiles(ctx, ctx.Package.Owner.ID); err != nil {
+               apiError(ctx, http.StatusInternalServerError, err)
+               return
+       }
+
+       ctx.Status(http.StatusCreated)
+}
+
+func DownloadPackageFile(ctx *context.Context) {
+       name := ctx.Params("name")
+       version := ctx.Params("version")
+
+       s, pf, err := packages_service.GetFileStreamByPackageNameAndVersion(
+               ctx,
+               &packages_service.PackageInfo{
+                       Owner:       ctx.Package.Owner,
+                       PackageType: packages_model.TypeRpm,
+                       Name:        name,
+                       Version:     version,
+               },
+               &packages_service.PackageFileInfo{
+                       Filename: fmt.Sprintf("%s-%s.%s.rpm", name, version, ctx.Params("architecture")),
+               },
+       )
+       if err != nil {
+               if errors.Is(err, util.ErrNotExist) {
+                       apiError(ctx, http.StatusNotFound, err)
+               } else {
+                       apiError(ctx, http.StatusInternalServerError, err)
+               }
+               return
+       }
+       defer s.Close()
+
+       ctx.ServeContent(s, &context.ServeHeaderOptions{
+               ContentType:  "application/x-rpm",
+               Filename:     pf.Name,
+               LastModified: pf.CreatedUnix.AsLocalTime(),
+       })
+}
+
+func DeletePackageFile(webctx *context.Context) {
+       name := webctx.Params("name")
+       version := webctx.Params("version")
+       architecture := webctx.Params("architecture")
+
+       var pd *packages_model.PackageDescriptor
+
+       err := db.WithTx(webctx, func(ctx stdctx.Context) error {
+               pv, err := packages_model.GetVersionByNameAndVersion(ctx, webctx.Package.Owner.ID, packages_model.TypeRpm, name, version)
+               if err != nil {
+                       return err
+               }
+
+               pf, err := packages_model.GetFileForVersionByName(
+                       ctx,
+                       pv.ID,
+                       fmt.Sprintf("%s-%s.%s.rpm", name, version, architecture),
+                       packages_model.EmptyFileKey,
+               )
+               if err != nil {
+                       return err
+               }
+
+               if err := packages_service.DeletePackageFile(ctx, pf); err != nil {
+                       return err
+               }
+
+               has, err := packages_model.HasVersionFileReferences(ctx, pv.ID)
+               if err != nil {
+                       return err
+               }
+               if !has {
+                       pd, err = packages_model.GetPackageDescriptor(ctx, pv)
+                       if err != nil {
+                               return err
+                       }
+
+                       if err := packages_service.DeletePackageVersionAndReferences(ctx, pv); err != nil {
+                               return err
+                       }
+               }
+
+               return nil
+       })
+       if err != nil {
+               if errors.Is(err, util.ErrNotExist) {
+                       apiError(webctx, http.StatusNotFound, err)
+               } else {
+                       apiError(webctx, http.StatusInternalServerError, err)
+               }
+               return
+       }
+
+       if pd != nil {
+               notification.NotifyPackageDelete(webctx, webctx.Doer, pd)
+       }
+
+       if err := rpm_service.BuildRepositoryFiles(webctx, webctx.Package.Owner.ID); err != nil {
+               apiError(webctx, http.StatusInternalServerError, err)
+               return
+       }
+
+       webctx.Status(http.StatusNoContent)
+}
index ac48eb8a53fec1d940b05cca750b5afefc852131..e0811f8665efd226307bd211a45ddd1bc44abd05 100644 (file)
@@ -40,7 +40,7 @@ func ListPackages(ctx *context.APIContext) {
        //   in: query
        //   description: package type filter
        //   type: string
-       //   enum: [cargo, chef, composer, conan, conda, container, debian, generic, helm, maven, npm, nuget, pub, pypi, rubygems, swift, vagrant]
+       //   enum: [cargo, chef, composer, conan, conda, container, debian, generic, helm, maven, npm, nuget, pub, pypi, rpm, rubygems, swift, vagrant]
        // - name: q
        //   in: query
        //   description: name filter
index efe4a0d84a24628f1ff5ef0b99acb0f2efd227e5..30296971079cb8dfcfa526d6badf04326be5d483 100644 (file)
@@ -15,7 +15,7 @@ import (
 type PackageCleanupRuleForm struct {
        ID            int64
        Enabled       bool
-       Type          string `binding:"Required;In(cargo,chef,composer,conan,conda,container,debian,generic,helm,maven,npm,nuget,pub,pypi,rubygems,swift,vagrant)"`
+       Type          string `binding:"Required;In(cargo,chef,composer,conan,conda,container,debian,generic,helm,maven,npm,nuget,pub,pypi,rpm,rubygems,swift,vagrant)"`
        KeepCount     int    `binding:"In(0,1,5,10,25,50,100)"`
        KeepPattern   string `binding:"RegexPattern"`
        RemoveDays    int    `binding:"In(0,7,14,30,60,90,180)"`
index 69d00086a0833c5d27e398e9d1bc9208d05b35e9..37ba47bdc301c4d89b6901f2f3e36da52d1551dc 100644 (file)
@@ -14,11 +14,9 @@ import (
        "strings"
        "time"
 
-       "code.gitea.io/gitea/models/db"
        packages_model "code.gitea.io/gitea/models/packages"
        debian_model "code.gitea.io/gitea/models/packages/debian"
        user_model "code.gitea.io/gitea/models/user"
-       "code.gitea.io/gitea/modules/log"
        packages_module "code.gitea.io/gitea/modules/packages"
        debian_module "code.gitea.io/gitea/modules/packages/debian"
        "code.gitea.io/gitea/modules/setting"
@@ -35,43 +33,7 @@ import (
 // GetOrCreateRepositoryVersion gets or creates the internal repository package
 // The Debian registry needs multiple index files which are stored in this package.
 func GetOrCreateRepositoryVersion(ownerID int64) (*packages_model.PackageVersion, error) {
-       var repositoryVersion *packages_model.PackageVersion
-
-       return repositoryVersion, db.WithTx(db.DefaultContext, func(ctx context.Context) error {
-               p := &packages_model.Package{
-                       OwnerID:    ownerID,
-                       Type:       packages_model.TypeDebian,
-                       Name:       debian_module.RepositoryPackage,
-                       LowerName:  debian_module.RepositoryPackage,
-                       IsInternal: true,
-               }
-               var err error
-               if p, err = packages_model.TryInsertPackage(ctx, p); err != nil {
-                       if err != packages_model.ErrDuplicatePackage {
-                               log.Error("Error inserting package: %v", err)
-                               return err
-                       }
-               }
-
-               pv := &packages_model.PackageVersion{
-                       PackageID:    p.ID,
-                       CreatorID:    ownerID,
-                       Version:      debian_module.RepositoryVersion,
-                       LowerVersion: debian_module.RepositoryVersion,
-                       IsInternal:   true,
-                       MetadataJSON: "null",
-               }
-               if pv, err = packages_model.GetOrInsertVersion(ctx, pv); err != nil {
-                       if err != packages_model.ErrDuplicatePackageVersion {
-                               log.Error("Error inserting package version: %v", err)
-                               return err
-                       }
-               }
-
-               repositoryVersion = pv
-
-               return nil
-       })
+       return packages_service.GetOrCreateInternalPackageVersion(ownerID, packages_model.TypeDebian, debian_module.RepositoryPackage, debian_module.RepositoryVersion)
 }
 
 // GetOrCreateKeyPair gets or creates the PGP keys used to sign repository files
index 735e52c854ba06097df2f3356bfb9f73edbb834c..535f2fac8e32fbabb83ea02b643afe21e9741e40 100644 (file)
@@ -379,6 +379,8 @@ func CheckSizeQuotaExceeded(ctx context.Context, doer, owner *user_model.User, p
                typeSpecificSize = setting.Packages.LimitSizePub
        case packages_model.TypePyPI:
                typeSpecificSize = setting.Packages.LimitSizePyPI
+       case packages_model.TypeRpm:
+               typeSpecificSize = setting.Packages.LimitSizeRpm
        case packages_model.TypeRubyGems:
                typeSpecificSize = setting.Packages.LimitSizeRubyGems
        case packages_model.TypeSwift:
@@ -406,6 +408,46 @@ func CheckSizeQuotaExceeded(ctx context.Context, doer, owner *user_model.User, p
        return nil
 }
 
+// GetOrCreateInternalPackageVersion gets or creates an internal package
+// Some package types need such internal packages for housekeeping.
+func GetOrCreateInternalPackageVersion(ownerID int64, packageType packages_model.Type, name, version string) (*packages_model.PackageVersion, error) {
+       var pv *packages_model.PackageVersion
+
+       return pv, db.WithTx(db.DefaultContext, func(ctx context.Context) error {
+               p := &packages_model.Package{
+                       OwnerID:    ownerID,
+                       Type:       packageType,
+                       Name:       name,
+                       LowerName:  name,
+                       IsInternal: true,
+               }
+               var err error
+               if p, err = packages_model.TryInsertPackage(ctx, p); err != nil {
+                       if err != packages_model.ErrDuplicatePackage {
+                               log.Error("Error inserting package: %v", err)
+                               return err
+                       }
+               }
+
+               pv = &packages_model.PackageVersion{
+                       PackageID:    p.ID,
+                       CreatorID:    ownerID,
+                       Version:      version,
+                       LowerVersion: version,
+                       IsInternal:   true,
+                       MetadataJSON: "null",
+               }
+               if pv, err = packages_model.GetOrInsertVersion(ctx, pv); err != nil {
+                       if err != packages_model.ErrDuplicatePackageVersion {
+                               log.Error("Error inserting package version: %v", err)
+                               return err
+                       }
+               }
+
+               return nil
+       })
+}
+
 // RemovePackageVersionByNameAndVersion deletes a package version and all associated files
 func RemovePackageVersionByNameAndVersion(doer *user_model.User, pvi *PackageInfo) error {
        pv, err := packages_model.GetVersionByNameAndVersion(db.DefaultContext, pvi.Owner.ID, pvi.PackageType, pvi.Name, pvi.Version)
diff --git a/services/packages/rpm/repository.go b/services/packages/rpm/repository.go
new file mode 100644 (file)
index 0000000..fb78e2b
--- /dev/null
@@ -0,0 +1,601 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package rpm
+
+import (
+       "bytes"
+       "compress/gzip"
+       "context"
+       "crypto/sha256"
+       "encoding/hex"
+       "encoding/xml"
+       "errors"
+       "fmt"
+       "io"
+       "net/url"
+       "strings"
+       "time"
+
+       packages_model "code.gitea.io/gitea/models/packages"
+       user_model "code.gitea.io/gitea/models/user"
+       "code.gitea.io/gitea/modules/json"
+       packages_module "code.gitea.io/gitea/modules/packages"
+       rpm_module "code.gitea.io/gitea/modules/packages/rpm"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/util"
+       packages_service "code.gitea.io/gitea/services/packages"
+
+       "github.com/keybase/go-crypto/openpgp"
+       "github.com/keybase/go-crypto/openpgp/armor"
+       "github.com/keybase/go-crypto/openpgp/packet"
+)
+
+// GetOrCreateRepositoryVersion gets or creates the internal repository package
+// The RPM registry needs multiple metadata files which are stored in this package.
+func GetOrCreateRepositoryVersion(ownerID int64) (*packages_model.PackageVersion, error) {
+       return packages_service.GetOrCreateInternalPackageVersion(ownerID, packages_model.TypeRpm, rpm_module.RepositoryPackage, rpm_module.RepositoryVersion)
+}
+
+// GetOrCreateKeyPair gets or creates the PGP keys used to sign repository metadata files
+func GetOrCreateKeyPair(ownerID int64) (string, string, error) {
+       priv, err := user_model.GetSetting(ownerID, rpm_module.SettingKeyPrivate)
+       if err != nil && !errors.Is(err, util.ErrNotExist) {
+               return "", "", err
+       }
+
+       pub, err := user_model.GetSetting(ownerID, rpm_module.SettingKeyPublic)
+       if err != nil && !errors.Is(err, util.ErrNotExist) {
+               return "", "", err
+       }
+
+       if priv == "" || pub == "" {
+               priv, pub, err = generateKeypair()
+               if err != nil {
+                       return "", "", err
+               }
+
+               if err := user_model.SetUserSetting(ownerID, rpm_module.SettingKeyPrivate, priv); err != nil {
+                       return "", "", err
+               }
+
+               if err := user_model.SetUserSetting(ownerID, rpm_module.SettingKeyPublic, pub); err != nil {
+                       return "", "", err
+               }
+       }
+
+       return priv, pub, nil
+}
+
+func generateKeypair() (string, string, error) {
+       e, err := openpgp.NewEntity(setting.AppName, "RPM Registry", "", nil)
+       if err != nil {
+               return "", "", err
+       }
+
+       var priv strings.Builder
+       var pub strings.Builder
+
+       w, err := armor.Encode(&priv, openpgp.PrivateKeyType, nil)
+       if err != nil {
+               return "", "", err
+       }
+       if err := e.SerializePrivate(w, nil); err != nil {
+               return "", "", err
+       }
+       w.Close()
+
+       w, err = armor.Encode(&pub, openpgp.PublicKeyType, nil)
+       if err != nil {
+               return "", "", err
+       }
+       if err := e.Serialize(w); err != nil {
+               return "", "", err
+       }
+       w.Close()
+
+       return priv.String(), pub.String(), nil
+}
+
+type repoChecksum struct {
+       Value string `xml:",chardata"`
+       Type  string `xml:"type,attr"`
+}
+
+type repoLocation struct {
+       Href string `xml:"href,attr"`
+}
+
+type repoData struct {
+       Type         string       `xml:"type,attr"`
+       Checksum     repoChecksum `xml:"checksum"`
+       OpenChecksum repoChecksum `xml:"open-checksum"`
+       Location     repoLocation `xml:"location"`
+       Timestamp    int64        `xml:"timestamp"`
+       Size         int64        `xml:"size"`
+       OpenSize     int64        `xml:"open-size"`
+}
+
+type packageData struct {
+       Package         *packages_model.Package
+       Version         *packages_model.PackageVersion
+       Blob            *packages_model.PackageBlob
+       VersionMetadata *rpm_module.VersionMetadata
+       FileMetadata    *rpm_module.FileMetadata
+}
+
+type packageCache = map[*packages_model.PackageFile]*packageData
+
+// BuildSpecificRepositoryFiles builds metadata files for the repository
+func BuildRepositoryFiles(ctx context.Context, ownerID int64) error {
+       pv, err := GetOrCreateRepositoryVersion(ownerID)
+       if err != nil {
+               return err
+       }
+
+       pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{
+               OwnerID:     ownerID,
+               PackageType: packages_model.TypeRpm,
+               Query:       "%.rpm",
+       })
+       if err != nil {
+               return err
+       }
+
+       // Delete the repository files if there are no packages
+       if len(pfs) == 0 {
+               pfs, err := packages_model.GetFilesByVersionID(ctx, pv.ID)
+               if err != nil {
+                       return err
+               }
+               for _, pf := range pfs {
+                       if err := packages_model.DeleteAllProperties(ctx, packages_model.PropertyTypeFile, pf.ID); err != nil {
+                               return err
+                       }
+                       if err := packages_model.DeleteFileByID(ctx, pf.ID); err != nil {
+                               return err
+                       }
+               }
+
+               return nil
+       }
+
+       // Cache data needed for all repository files
+       cache := make(packageCache)
+       for _, pf := range pfs {
+               pv, err := packages_model.GetVersionByID(ctx, pf.VersionID)
+               if err != nil {
+                       return err
+               }
+               p, err := packages_model.GetPackageByID(ctx, pv.PackageID)
+               if err != nil {
+                       return err
+               }
+               pb, err := packages_model.GetBlobByID(ctx, pf.BlobID)
+               if err != nil {
+                       return err
+               }
+               pps, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeFile, pf.ID, rpm_module.PropertyMetadata)
+               if err != nil {
+                       return err
+               }
+
+               pd := &packageData{
+                       Package: p,
+                       Version: pv,
+                       Blob:    pb,
+               }
+
+               if err := json.Unmarshal([]byte(pv.MetadataJSON), &pd.VersionMetadata); err != nil {
+                       return err
+               }
+               if len(pps) > 0 {
+                       if err := json.Unmarshal([]byte(pps[0].Value), &pd.FileMetadata); err != nil {
+                               return err
+                       }
+               }
+
+               cache[pf] = pd
+       }
+
+       primary, err := buildPrimary(pv, pfs, cache)
+       if err != nil {
+               return err
+       }
+       filelists, err := buildFilelists(pv, pfs, cache)
+       if err != nil {
+               return err
+       }
+       other, err := buildOther(pv, pfs, cache)
+       if err != nil {
+               return err
+       }
+
+       return buildRepomd(
+               pv,
+               ownerID,
+               []*repoData{
+                       primary,
+                       filelists,
+                       other,
+               },
+       )
+}
+
+// https://docs.pulpproject.org/en/2.19/plugins/pulp_rpm/tech-reference/rpm.html#repomd-xml
+func buildRepomd(pv *packages_model.PackageVersion, ownerID int64, data []*repoData) error {
+       type Repomd struct {
+               XMLName  xml.Name    `xml:"repomd"`
+               Xmlns    string      `xml:"xmlns,attr"`
+               XmlnsRpm string      `xml:"xmlns:rpm,attr"`
+               Data     []*repoData `xml:"data"`
+       }
+
+       var buf bytes.Buffer
+       buf.Write([]byte(xml.Header))
+       if err := xml.NewEncoder(&buf).Encode(&Repomd{
+               Xmlns:    "http://linux.duke.edu/metadata/repo",
+               XmlnsRpm: "http://linux.duke.edu/metadata/rpm",
+               Data:     data,
+       }); err != nil {
+               return err
+       }
+
+       priv, _, err := GetOrCreateKeyPair(ownerID)
+       if err != nil {
+               return err
+       }
+
+       block, err := armor.Decode(strings.NewReader(priv))
+       if err != nil {
+               return err
+       }
+
+       e, err := openpgp.ReadEntity(packet.NewReader(block.Body))
+       if err != nil {
+               return err
+       }
+
+       repomdAscContent, _ := packages_module.NewHashedBuffer()
+       if err := openpgp.ArmoredDetachSign(repomdAscContent, e, bytes.NewReader(buf.Bytes()), nil); err != nil {
+               return err
+       }
+
+       repomdContent, _ := packages_module.CreateHashedBufferFromReader(&buf)
+
+       for _, file := range []struct {
+               Name string
+               Data packages_module.HashedSizeReader
+       }{
+               {"repomd.xml", repomdContent},
+               {"repomd.xml.asc", repomdAscContent},
+       } {
+               _, err = packages_service.AddFileToPackageVersionInternal(
+                       pv,
+                       &packages_service.PackageFileCreationInfo{
+                               PackageFileInfo: packages_service.PackageFileInfo{
+                                       Filename: file.Name,
+                               },
+                               Creator:           user_model.NewGhostUser(),
+                               Data:              file.Data,
+                               IsLead:            false,
+                               OverwriteExisting: true,
+                       },
+               )
+               if err != nil {
+                       return err
+               }
+       }
+
+       return nil
+}
+
+// https://docs.pulpproject.org/en/2.19/plugins/pulp_rpm/tech-reference/rpm.html#primary-xml
+func buildPrimary(pv *packages_model.PackageVersion, pfs []*packages_model.PackageFile, c packageCache) (*repoData, error) {
+       type Version struct {
+               Epoch   string `xml:"epoch,attr"`
+               Version string `xml:"ver,attr"`
+               Release string `xml:"rel,attr"`
+       }
+
+       type Checksum struct {
+               Checksum string `xml:",chardata"`
+               Type     string `xml:"type,attr"`
+               Pkgid    string `xml:"pkgid,attr"`
+       }
+
+       type Times struct {
+               File  uint64 `xml:"file,attr"`
+               Build uint64 `xml:"build,attr"`
+       }
+
+       type Sizes struct {
+               Package   int64  `xml:"package,attr"`
+               Installed uint64 `xml:"installed,attr"`
+               Archive   uint64 `xml:"archive,attr"`
+       }
+
+       type Location struct {
+               Href string `xml:"href,attr"`
+       }
+
+       type EntryList struct {
+               Entries []*rpm_module.Entry `xml:"rpm:entry"`
+       }
+
+       type Format struct {
+               License   string             `xml:"rpm:license"`
+               Vendor    string             `xml:"rpm:vendor"`
+               Group     string             `xml:"rpm:group"`
+               Buildhost string             `xml:"rpm:buildhost"`
+               Sourcerpm string             `xml:"rpm:sourcerpm"`
+               Provides  EntryList          `xml:"rpm:provides"`
+               Requires  EntryList          `xml:"rpm:requires"`
+               Conflicts EntryList          `xml:"rpm:conflicts"`
+               Obsoletes EntryList          `xml:"rpm:obsoletes"`
+               Files     []*rpm_module.File `xml:"file"`
+       }
+
+       type Package struct {
+               XMLName      xml.Name `xml:"package"`
+               Type         string   `xml:"type,attr"`
+               Name         string   `xml:"name"`
+               Architecture string   `xml:"arch"`
+               Version      Version  `xml:"version"`
+               Checksum     Checksum `xml:"checksum"`
+               Summary      string   `xml:"summary"`
+               Description  string   `xml:"description"`
+               Packager     string   `xml:"packager"`
+               URL          string   `xml:"url"`
+               Time         Times    `xml:"time"`
+               Size         Sizes    `xml:"size"`
+               Location     Location `xml:"location"`
+               Format       Format   `xml:"format"`
+       }
+
+       type Metadata struct {
+               XMLName      xml.Name   `xml:"metadata"`
+               Xmlns        string     `xml:"xmlns,attr"`
+               XmlnsRpm     string     `xml:"xmlns:rpm,attr"`
+               PackageCount int        `xml:"packages,attr"`
+               Packages     []*Package `xml:"package"`
+       }
+
+       packages := make([]*Package, 0, len(pfs))
+       for _, pf := range pfs {
+               pd := c[pf]
+
+               files := make([]*rpm_module.File, 0, 3)
+               for _, f := range pd.FileMetadata.Files {
+                       if f.IsExecutable {
+                               files = append(files, f)
+                       }
+               }
+
+               packages = append(packages, &Package{
+                       Type:         "rpm",
+                       Name:         pd.Package.Name,
+                       Architecture: pd.FileMetadata.Architecture,
+                       Version: Version{
+                               Epoch:   pd.FileMetadata.Epoch,
+                               Version: pd.Version.Version,
+                               Release: pd.FileMetadata.Release,
+                       },
+                       Checksum: Checksum{
+                               Type:     "sha256",
+                               Checksum: pd.Blob.HashSHA256,
+                               Pkgid:    "YES",
+                       },
+                       Summary:     pd.VersionMetadata.Summary,
+                       Description: pd.VersionMetadata.Description,
+                       Packager:    pd.FileMetadata.Packager,
+                       URL:         pd.VersionMetadata.ProjectURL,
+                       Time: Times{
+                               File:  pd.FileMetadata.FileTime,
+                               Build: pd.FileMetadata.BuildTime,
+                       },
+                       Size: Sizes{
+                               Package:   pd.Blob.Size,
+                               Installed: pd.FileMetadata.InstalledSize,
+                               Archive:   pd.FileMetadata.ArchiveSize,
+                       },
+                       Location: Location{
+                               Href: fmt.Sprintf("package/%s/%s/%s", url.PathEscape(pd.Package.Name), url.PathEscape(pd.Version.Version), url.PathEscape(pd.FileMetadata.Architecture)),
+                       },
+                       Format: Format{
+                               License:   pd.VersionMetadata.License,
+                               Vendor:    pd.FileMetadata.Vendor,
+                               Group:     pd.FileMetadata.Group,
+                               Buildhost: pd.FileMetadata.BuildHost,
+                               Sourcerpm: pd.FileMetadata.SourceRpm,
+                               Provides: EntryList{
+                                       Entries: pd.FileMetadata.Provides,
+                               },
+                               Requires: EntryList{
+                                       Entries: pd.FileMetadata.Requires,
+                               },
+                               Conflicts: EntryList{
+                                       Entries: pd.FileMetadata.Conflicts,
+                               },
+                               Obsoletes: EntryList{
+                                       Entries: pd.FileMetadata.Obsoletes,
+                               },
+                               Files: files,
+                       },
+               })
+       }
+
+       return addDataAsFileToRepo(pv, "primary", &Metadata{
+               Xmlns:        "http://linux.duke.edu/metadata/common",
+               XmlnsRpm:     "http://linux.duke.edu/metadata/rpm",
+               PackageCount: len(pfs),
+               Packages:     packages,
+       })
+}
+
+// https://docs.pulpproject.org/en/2.19/plugins/pulp_rpm/tech-reference/rpm.html#filelists-xml
+func buildFilelists(pv *packages_model.PackageVersion, pfs []*packages_model.PackageFile, c packageCache) (*repoData, error) { //nolint:dupl
+       type Version struct {
+               Epoch   string `xml:"epoch,attr"`
+               Version string `xml:"ver,attr"`
+               Release string `xml:"rel,attr"`
+       }
+
+       type Package struct {
+               Pkgid        string             `xml:"pkgid,attr"`
+               Name         string             `xml:"name,attr"`
+               Architecture string             `xml:"arch,attr"`
+               Version      Version            `xml:"version"`
+               Files        []*rpm_module.File `xml:"file"`
+       }
+
+       type Filelists struct {
+               XMLName      xml.Name   `xml:"filelists"`
+               Xmlns        string     `xml:"xmlns,attr"`
+               PackageCount int        `xml:"packages,attr"`
+               Packages     []*Package `xml:"package"`
+       }
+
+       packages := make([]*Package, 0, len(pfs))
+       for _, pf := range pfs {
+               pd := c[pf]
+
+               packages = append(packages, &Package{
+                       Pkgid:        pd.Blob.HashSHA256,
+                       Name:         pd.Package.Name,
+                       Architecture: pd.FileMetadata.Architecture,
+                       Version: Version{
+                               Epoch:   pd.FileMetadata.Epoch,
+                               Version: pd.Version.Version,
+                               Release: pd.FileMetadata.Release,
+                       },
+                       Files: pd.FileMetadata.Files,
+               })
+       }
+
+       return addDataAsFileToRepo(pv, "filelists", &Filelists{
+               Xmlns:        "http://linux.duke.edu/metadata/other",
+               PackageCount: len(pfs),
+               Packages:     packages,
+       })
+}
+
+// https://docs.pulpproject.org/en/2.19/plugins/pulp_rpm/tech-reference/rpm.html#other-xml
+func buildOther(pv *packages_model.PackageVersion, pfs []*packages_model.PackageFile, c packageCache) (*repoData, error) { //nolint:dupl
+       type Version struct {
+               Epoch   string `xml:"epoch,attr"`
+               Version string `xml:"ver,attr"`
+               Release string `xml:"rel,attr"`
+       }
+
+       type Package struct {
+               Pkgid        string                  `xml:"pkgid,attr"`
+               Name         string                  `xml:"name,attr"`
+               Architecture string                  `xml:"arch,attr"`
+               Version      Version                 `xml:"version"`
+               Changelogs   []*rpm_module.Changelog `xml:"changelog"`
+       }
+
+       type Otherdata struct {
+               XMLName      xml.Name   `xml:"otherdata"`
+               Xmlns        string     `xml:"xmlns,attr"`
+               PackageCount int        `xml:"packages,attr"`
+               Packages     []*Package `xml:"package"`
+       }
+
+       packages := make([]*Package, 0, len(pfs))
+       for _, pf := range pfs {
+               pd := c[pf]
+
+               packages = append(packages, &Package{
+                       Pkgid:        pd.Blob.HashSHA256,
+                       Name:         pd.Package.Name,
+                       Architecture: pd.FileMetadata.Architecture,
+                       Version: Version{
+                               Epoch:   pd.FileMetadata.Epoch,
+                               Version: pd.Version.Version,
+                               Release: pd.FileMetadata.Release,
+                       },
+                       Changelogs: pd.FileMetadata.Changelogs,
+               })
+       }
+
+       return addDataAsFileToRepo(pv, "other", &Otherdata{
+               Xmlns:        "http://linux.duke.edu/metadata/other",
+               PackageCount: len(pfs),
+               Packages:     packages,
+       })
+}
+
+// writtenCounter counts all written bytes
+type writtenCounter struct {
+       written int64
+}
+
+func (wc *writtenCounter) Write(buf []byte) (int, error) {
+       n := len(buf)
+
+       wc.written += int64(n)
+
+       return n, nil
+}
+
+func (wc *writtenCounter) Written() int64 {
+       return wc.written
+}
+
+func addDataAsFileToRepo(pv *packages_model.PackageVersion, filetype string, obj any) (*repoData, error) {
+       content, _ := packages_module.NewHashedBuffer()
+       gzw := gzip.NewWriter(content)
+       wc := &writtenCounter{}
+       h := sha256.New()
+
+       w := io.MultiWriter(gzw, wc, h)
+       _, _ = w.Write([]byte(xml.Header))
+
+       if err := xml.NewEncoder(w).Encode(obj); err != nil {
+               return nil, err
+       }
+
+       if err := gzw.Close(); err != nil {
+               return nil, err
+       }
+
+       filename := filetype + ".xml.gz"
+
+       _, err := packages_service.AddFileToPackageVersionInternal(
+               pv,
+               &packages_service.PackageFileCreationInfo{
+                       PackageFileInfo: packages_service.PackageFileInfo{
+                               Filename: filename,
+                       },
+                       Creator:           user_model.NewGhostUser(),
+                       Data:              content,
+                       IsLead:            false,
+                       OverwriteExisting: true,
+               },
+       )
+       if err != nil {
+               return nil, err
+       }
+
+       _, _, hashSHA256, _ := content.Sums()
+
+       return &repoData{
+               Type: filetype,
+               Checksum: repoChecksum{
+                       Type:  "sha256",
+                       Value: hex.EncodeToString(hashSHA256),
+               },
+               OpenChecksum: repoChecksum{
+                       Type:  "sha256",
+                       Value: hex.EncodeToString(h.Sum(nil)),
+               },
+               Location: repoLocation{
+                       Href: "repodata/" + filename,
+               },
+               Timestamp: time.Now().Unix(),
+               Size:      content.Size(),
+               OpenSize:  wc.Written(),
+       }, nil
+}
diff --git a/templates/package/content/rpm.tmpl b/templates/package/content/rpm.tmpl
new file mode 100644 (file)
index 0000000..49a9273
--- /dev/null
@@ -0,0 +1,26 @@
+{{if eq .PackageDescriptor.Package.Type "rpm"}}
+       <h4 class="ui top attached header">{{.locale.Tr "packages.installation"}}</h4>
+       <div class="ui attached segment">
+               <div class="ui form">
+                       <div class="field">
+                               <label>{{svg "octicon-terminal"}} {{.locale.Tr "packages.rpm.registry"}}</label>
+                               <div class="markup"><pre class="code-block"><code>dnf config-manager --add-repo <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/rpm/{{$.PackageDescriptor.Owner.LowerName}}.repo"></gitea-origin-url></code></pre></div>
+                       </div>
+                       <div class="field">
+                               <label>{{svg "octicon-terminal"}} {{.locale.Tr "packages.rpm.install"}}</label>
+                               <div class="markup">
+                                       <pre class="code-block"><code>dnf install {{$.PackageDescriptor.Package.Name}}</code></pre>
+                               </div>
+                       </div>
+                       <div class="field">
+                               <label>{{.locale.Tr "packages.rpm.documentation" "https://docs.gitea.io/en-us/usage/packages/rpm/" | Safe}}</label>
+                       </div>
+               </div>
+       </div>
+
+       {{if or .PackageDescriptor.Metadata.Summary .PackageDescriptor.Metadata.Description}}
+               <h4 class="ui top attached header">{{.locale.Tr "packages.about"}}</h4>
+               {{if .PackageDescriptor.Metadata.Summary}}<div class="ui attached segment">{{.PackageDescriptor.Metadata.Summary}}</div>{{end}}
+               {{if .PackageDescriptor.Metadata.Description}}<div class="ui attached segment">{{.PackageDescriptor.Metadata.Description}}</div>{{end}}
+       {{end}}
+{{end}}
diff --git a/templates/package/metadata/rpm.tmpl b/templates/package/metadata/rpm.tmpl
new file mode 100644 (file)
index 0000000..1bbc2c8
--- /dev/null
@@ -0,0 +1,4 @@
+{{if eq .PackageDescriptor.Package.Type "rpm"}}
+       {{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{.locale.Tr "packages.details.project_site"}}</a></div>{{end}}
+       {{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{.locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
+{{end}}
index 30e737d09ceae7fd69b0f3a4f600c55d4c2db1da..36637772cdbb5cedfbda0c2239d781186959827a 100644 (file)
@@ -33,6 +33,7 @@
                                        {{template "package/content/nuget" .}}
                                        {{template "package/content/pub" .}}
                                        {{template "package/content/pypi" .}}
+                                       {{template "package/content/rpm" .}}
                                        {{template "package/content/rubygems" .}}
                                        {{template "package/content/swift" .}}
                                        {{template "package/content/vagrant" .}}
@@ -61,6 +62,7 @@
                                                        {{template "package/metadata/nuget" .}}
                                                        {{template "package/metadata/pub" .}}
                                                        {{template "package/metadata/pypi" .}}
+                                                       {{template "package/metadata/rpm" .}}
                                                        {{template "package/metadata/rubygems" .}}
                                                        {{template "package/metadata/swift" .}}
                                                        {{template "package/metadata/vagrant" .}}
index 34a4101ed33fef2bbf2bb9b7c63e2925a4d3f616..7923e93ca4d236e055f4fed16a13f945a8dab54e 100644 (file)
               "nuget",
               "pub",
               "pypi",
+              "rpm",
               "rubygems",
               "swift",
               "vagrant"
diff --git a/tests/integration/api_packages_rpm_test.go b/tests/integration/api_packages_rpm_test.go
new file mode 100644 (file)
index 0000000..740bdf8
--- /dev/null
@@ -0,0 +1,413 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package integration
+
+import (
+       "bytes"
+       "compress/gzip"
+       "encoding/base64"
+       "encoding/xml"
+       "fmt"
+       "io"
+       "net/http"
+       "net/http/httptest"
+       "testing"
+
+       "code.gitea.io/gitea/models/db"
+       "code.gitea.io/gitea/models/packages"
+       "code.gitea.io/gitea/models/unittest"
+       user_model "code.gitea.io/gitea/models/user"
+       rpm_module "code.gitea.io/gitea/modules/packages/rpm"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/tests"
+
+       "github.com/stretchr/testify/assert"
+)
+
+func TestPackageRpm(t *testing.T) {
+       defer tests.PrepareTestEnv(t)()
+
+       packageName := "gitea-test"
+       packageVersion := "1.0.2-1"
+       packageArchitecture := "x86_64"
+
+       user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+
+       base64RpmPackageContent := `H4sICFayB2QCAGdpdGVhLXRlc3QtMS4wLjItMS14ODZfNjQucnBtAO2YV4gTQRjHJzl7wbNhhxVF
+VNwk2zd2PdvZ9Sxnd3Z3NllNsmF3o6congVFsWFHRWwIImIXfRER0QcRfPBJEXvvBQvWSfZTT0VQ
+8TF/MuU33zcz3+zOJGEe73lyuQBRBWKWRzDrEddjuVAkxLMc+lsFUOWfm5bvvReAalWECg/TsivU
+dyKa0U61aVnl6wj0Uxe4nc8F92hZiaYE8CO/P0r7/Quegr0c7M/AvoCaGZEIWNGUqMHrhhGROIUT
+Zc7gOAOraoQzCNZ0WdU0HpEI5jiB4zlek3gT85wqCBomhomxoGCs8wImWMImbxqKgXVNUKKaqShR
+STKVKK9glFUNcf2g+/t27xs16v5x/eyOKftVGlIhyiuvvPLKK6+88sorr7zyyiuvvPKCO5HPnz+v
+pGVhhXsTsFVeSstuWR9anwU+Bk3Vch5wTwL3JkHg+8C1gR8A169wj1KdpobAj4HbAT+Be5VewE+h
+fz/g52AvBX4N9vHAb4AnA7+F8ePAH8BuA38ELgf+BLzQ50oIeBlw0OdAOXAlP57AGuCsbwGtbgCu
+DrwRuAb4bwau6T/PwFbgWsDXgWuD/y3gOmC/B1wI/Bi4AcT3Arih3z9YCNzI9w9m/YKUG4Nd9N9z
+pSZgHwrcFPgccFt//OADGE+F/q+Ao+D/FrijzwV1gbv4/QvaAHcFDgF3B5aB+wB3Be7rz1dQCtwP
+eDxwMcw3GbgU7AasdwzYE8DjwT4L/CeAvRx4IvBCYA3iWQds+FzpDjABfghsAj8BTgA/A/b8+StX
+A84A1wKe5s9fuRB4JpzHZv55rL8a/Dv49vpn/PErR4BvQX8Z+Db4l2W5CH2/f0W5+1fEoeFDBzFp
+rE/FMcK4mWQSOzN+aDOIqztW2rPsFKIyqh7sQERR42RVMSKihnzVHlQ8Ag0YLBYNEIajkhmuR5Io
+7nlpt2M4nJs0ZNkoYaUyZahMlSfJImr1n1WjFVNCPCaTZgYNGdGL8YN2mX8WHfA/C7ViHJK0pxHG
+SrkeTiSI4T+7ubf85yrzRCQRQ5EVxVAjvIBVRY/KRFAVReIkhfARSddNSceayQkGliIKb0q8RAxJ
+5QWNVxHIsW3Pz369bw+5jh5y0klE9Znqm0dF57b0HbGy2A5lVUBTZZrqZjdUjYoprFmpsBtHP5d0
++ISltS2yk2mHuC4x+lgJMhgnidvuqy3b0suK0bm+tw3FMxI2zjm7/fA0MtQhplX2s7nYLZ2ZC0yg
+CxJZDokhORTJlrlcCvG5OieGBERlVCs7CfuS6WzQ/T2j+9f92BWxTFEcp2IkYccYGp2LYySEfreq
+irue4WRF5XkpKovw2wgpq2rZBI8bQZkzxEkiYaNwxnXCCVvHidzIiB3CM2yMYdNWmjDsaLovaE4c
+x3a6mLaTxB7rEj3jWN4M2p7uwPaa1GfI8BHFfcZMKhkycnhR7y781/a+A4t7FpWWTupRUtKbegwZ
+XMKwJinTSe70uhRcj55qNu3YHtE922Fdz7FTMTq9Q3TbMdiYrrPudMvT44S6u2miu138eC0tTN9D
+2CFGHHtQsHHsGCRFDFbXuT9wx6mUTZfseydlkWZeJkW6xOgYjqXT+LA7I6XHaUx2xmUzqelWymA9
+rCXI9+D1BHbjsITssqhBNysw0tOWjcpmIh6+aViYPfftw8ZSGfRVPUqKiosZj5R5qGmk/8AjjRbZ
+d8b3vvngdPHx3HvMeCarIk7VVSwbgoZVkceEVyOmyUmGxBGNYDVKSFSOGlIkGqWnUZFkiY/wsmhK
+Mu0UFYgZ/bYnuvn/vz4wtCz8qMwsHUvP0PX3tbYFUctAPdrY6tiiDtcCddDECahx7SuVNP5dpmb5
+9tMDyaXb7OAlk5acuPn57ss9mw6Wym0m1Fq2cej7tUt2LL4/b8enXU2fndk+fvv57ndnt55/cQob
+7tpp/pEjDS7cGPZ6BY430+7danDq6f42Nw49b9F7zp6BiKpJb9s5P0AYN2+L159cnrur636rx+v1
+7ae1K28QbMMcqI8CqwIrgwg9nTOp8Oj9q81plUY7ZuwXN8Vvs8wbAAA=`
+       rpmPackageContent, err := base64.StdEncoding.DecodeString(base64RpmPackageContent)
+       assert.NoError(t, err)
+
+       zr, err := gzip.NewReader(bytes.NewReader(rpmPackageContent))
+       assert.NoError(t, err)
+
+       content, err := io.ReadAll(zr)
+       assert.NoError(t, err)
+
+       rootURL := fmt.Sprintf("/api/packages/%s/rpm", user.Name)
+
+       t.Run("RepositoryConfig", func(t *testing.T) {
+               defer tests.PrintCurrentTest(t)()
+
+               req := NewRequest(t, "GET", rootURL+".repo")
+               resp := MakeRequest(t, req, http.StatusOK)
+
+               expected := fmt.Sprintf(`[gitea-%s]
+name=%s - %s
+baseurl=%sapi/packages/%s/rpm
+enabled=1
+gpgcheck=1
+gpgkey=%sapi/packages/%s/rpm/repository.key`, user.Name, user.Name, setting.AppName, setting.AppURL, user.Name, setting.AppURL, user.Name)
+
+               assert.Equal(t, expected, resp.Body.String())
+       })
+
+       t.Run("RepositoryKey", func(t *testing.T) {
+               defer tests.PrintCurrentTest(t)()
+
+               req := NewRequest(t, "GET", rootURL+"/repository.key")
+               resp := MakeRequest(t, req, http.StatusOK)
+
+               assert.Equal(t, "application/pgp-keys", resp.Header().Get("Content-Type"))
+               assert.Contains(t, resp.Body.String(), "-----BEGIN PGP PUBLIC KEY BLOCK-----")
+       })
+
+       t.Run("Upload", func(t *testing.T) {
+               url := rootURL + "/upload"
+
+               req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(content))
+               MakeRequest(t, req, http.StatusUnauthorized)
+
+               req = NewRequestWithBody(t, "PUT", url, bytes.NewReader(content))
+               req = AddBasicAuthHeader(req, user.Name)
+               MakeRequest(t, req, http.StatusCreated)
+
+               pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeRpm)
+               assert.NoError(t, err)
+               assert.Len(t, pvs, 1)
+
+               pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
+               assert.NoError(t, err)
+               assert.Nil(t, pd.SemVer)
+               assert.IsType(t, &rpm_module.VersionMetadata{}, pd.Metadata)
+               assert.Equal(t, packageName, pd.Package.Name)
+               assert.Equal(t, packageVersion, pd.Version.Version)
+
+               pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
+               assert.NoError(t, err)
+               assert.Len(t, pfs, 1)
+               assert.Equal(t, fmt.Sprintf("%s-%s.%s.rpm", packageName, packageVersion, packageArchitecture), pfs[0].Name)
+               assert.True(t, pfs[0].IsLead)
+
+               pb, err := packages.GetBlobByID(db.DefaultContext, pfs[0].BlobID)
+               assert.NoError(t, err)
+               assert.Equal(t, int64(len(content)), pb.Size)
+
+               req = NewRequestWithBody(t, "PUT", url, bytes.NewReader(content))
+               req = AddBasicAuthHeader(req, user.Name)
+               MakeRequest(t, req, http.StatusConflict)
+       })
+
+       t.Run("Download", func(t *testing.T) {
+               defer tests.PrintCurrentTest(t)()
+
+               req := NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/%s/%s", rootURL, packageName, packageVersion, packageArchitecture))
+               resp := MakeRequest(t, req, http.StatusOK)
+
+               assert.Equal(t, content, resp.Body.Bytes())
+       })
+
+       t.Run("Repository", func(t *testing.T) {
+               defer tests.PrintCurrentTest(t)()
+
+               url := rootURL + "/repodata"
+
+               req := NewRequest(t, "GET", url+"/dummy.xml")
+               MakeRequest(t, req, http.StatusNotFound)
+
+               t.Run("repomd.xml", func(t *testing.T) {
+                       defer tests.PrintCurrentTest(t)()
+
+                       req = NewRequest(t, "GET", url+"/repomd.xml")
+                       resp := MakeRequest(t, req, http.StatusOK)
+
+                       type Repomd struct {
+                               XMLName  xml.Name `xml:"repomd"`
+                               Xmlns    string   `xml:"xmlns,attr"`
+                               XmlnsRpm string   `xml:"xmlns:rpm,attr"`
+                               Data     []struct {
+                                       Type     string `xml:"type,attr"`
+                                       Checksum struct {
+                                               Value string `xml:",chardata"`
+                                               Type  string `xml:"type,attr"`
+                                       } `xml:"checksum"`
+                                       OpenChecksum struct {
+                                               Value string `xml:",chardata"`
+                                               Type  string `xml:"type,attr"`
+                                       } `xml:"open-checksum"`
+                                       Location struct {
+                                               Href string `xml:"href,attr"`
+                                       } `xml:"location"`
+                                       Timestamp int64 `xml:"timestamp"`
+                                       Size      int64 `xml:"size"`
+                                       OpenSize  int64 `xml:"open-size"`
+                               } `xml:"data"`
+                       }
+
+                       var result Repomd
+                       decodeXML(t, resp, &result)
+
+                       assert.Len(t, result.Data, 3)
+                       for _, d := range result.Data {
+                               assert.Equal(t, "sha256", d.Checksum.Type)
+                               assert.NotEmpty(t, d.Checksum.Value)
+                               assert.Equal(t, "sha256", d.OpenChecksum.Type)
+                               assert.NotEmpty(t, d.OpenChecksum.Value)
+                               assert.NotEqual(t, d.Checksum.Value, d.OpenChecksum.Value)
+                               assert.Greater(t, d.OpenSize, d.Size)
+
+                               switch d.Type {
+                               case "primary":
+                                       assert.EqualValues(t, 718, d.Size)
+                                       assert.EqualValues(t, 1731, d.OpenSize)
+                                       assert.Equal(t, "repodata/primary.xml.gz", d.Location.Href)
+                               case "filelists":
+                                       assert.EqualValues(t, 258, d.Size)
+                                       assert.EqualValues(t, 328, d.OpenSize)
+                                       assert.Equal(t, "repodata/filelists.xml.gz", d.Location.Href)
+                               case "other":
+                                       assert.EqualValues(t, 308, d.Size)
+                                       assert.EqualValues(t, 396, d.OpenSize)
+                                       assert.Equal(t, "repodata/other.xml.gz", d.Location.Href)
+                               }
+                       }
+               })
+
+               t.Run("repomd.xml.asc", func(t *testing.T) {
+                       defer tests.PrintCurrentTest(t)()
+
+                       req = NewRequest(t, "GET", url+"/repomd.xml.asc")
+                       resp := MakeRequest(t, req, http.StatusOK)
+
+                       assert.Contains(t, resp.Body.String(), "-----BEGIN PGP SIGNATURE-----")
+               })
+
+               decodeGzipXML := func(t testing.TB, resp *httptest.ResponseRecorder, v interface{}) {
+                       t.Helper()
+
+                       zr, err := gzip.NewReader(resp.Body)
+                       assert.NoError(t, err)
+
+                       assert.NoError(t, xml.NewDecoder(zr).Decode(v))
+               }
+
+               t.Run("primary.xml.gz", func(t *testing.T) {
+                       defer tests.PrintCurrentTest(t)()
+
+                       req = NewRequest(t, "GET", url+"/primary.xml.gz")
+                       resp := MakeRequest(t, req, http.StatusOK)
+
+                       type EntryList struct {
+                               Entries []*rpm_module.Entry `xml:"entry"`
+                       }
+
+                       type Metadata struct {
+                               XMLName      xml.Name `xml:"metadata"`
+                               Xmlns        string   `xml:"xmlns,attr"`
+                               XmlnsRpm     string   `xml:"xmlns:rpm,attr"`
+                               PackageCount int      `xml:"packages,attr"`
+                               Packages     []struct {
+                                       XMLName      xml.Name `xml:"package"`
+                                       Type         string   `xml:"type,attr"`
+                                       Name         string   `xml:"name"`
+                                       Architecture string   `xml:"arch"`
+                                       Version      struct {
+                                               Epoch   string `xml:"epoch,attr"`
+                                               Version string `xml:"ver,attr"`
+                                               Release string `xml:"rel,attr"`
+                                       } `xml:"version"`
+                                       Checksum struct {
+                                               Checksum string `xml:",chardata"`
+                                               Type     string `xml:"type,attr"`
+                                               Pkgid    string `xml:"pkgid,attr"`
+                                       } `xml:"checksum"`
+                                       Summary     string `xml:"summary"`
+                                       Description string `xml:"description"`
+                                       Packager    string `xml:"packager"`
+                                       URL         string `xml:"url"`
+                                       Time        struct {
+                                               File  uint64 `xml:"file,attr"`
+                                               Build uint64 `xml:"build,attr"`
+                                       } `xml:"time"`
+                                       Size struct {
+                                               Package   int64  `xml:"package,attr"`
+                                               Installed uint64 `xml:"installed,attr"`
+                                               Archive   uint64 `xml:"archive,attr"`
+                                       } `xml:"size"`
+                                       Location struct {
+                                               Href string `xml:"href,attr"`
+                                       } `xml:"location"`
+                                       Format struct {
+                                               License   string             `xml:"license"`
+                                               Vendor    string             `xml:"vendor"`
+                                               Group     string             `xml:"group"`
+                                               Buildhost string             `xml:"buildhost"`
+                                               Sourcerpm string             `xml:"sourcerpm"`
+                                               Provides  EntryList          `xml:"provides"`
+                                               Requires  EntryList          `xml:"requires"`
+                                               Conflicts EntryList          `xml:"conflicts"`
+                                               Obsoletes EntryList          `xml:"obsoletes"`
+                                               Files     []*rpm_module.File `xml:"file"`
+                                       } `xml:"format"`
+                               } `xml:"package"`
+                       }
+
+                       var result Metadata
+                       decodeGzipXML(t, resp, &result)
+
+                       assert.EqualValues(t, 1, result.PackageCount)
+                       assert.Len(t, result.Packages, 1)
+                       p := result.Packages[0]
+                       assert.Equal(t, "rpm", p.Type)
+                       assert.Equal(t, packageName, p.Name)
+                       assert.Equal(t, packageArchitecture, p.Architecture)
+                       assert.Equal(t, "YES", p.Checksum.Pkgid)
+                       assert.Equal(t, "sha256", p.Checksum.Type)
+                       assert.Equal(t, "f1d5d2ffcbe4a7568e98b864f40d923ecca084e9b9bcd5977ed6521c46d3fa4c", p.Checksum.Checksum)
+                       assert.Equal(t, "https://gitea.io", p.URL)
+                       assert.EqualValues(t, len(content), p.Size.Package)
+                       assert.EqualValues(t, 13, p.Size.Installed)
+                       assert.EqualValues(t, 272, p.Size.Archive)
+                       assert.Equal(t, fmt.Sprintf("package/%s/%s/%s", packageName, packageVersion, packageArchitecture), p.Location.Href)
+                       f := p.Format
+                       assert.Equal(t, "MIT", f.License)
+                       assert.Len(t, f.Provides.Entries, 2)
+                       assert.Len(t, f.Requires.Entries, 7)
+                       assert.Empty(t, f.Conflicts.Entries)
+                       assert.Empty(t, f.Obsoletes.Entries)
+                       assert.Len(t, f.Files, 1)
+               })
+
+               t.Run("filelists.xml.gz", func(t *testing.T) {
+                       defer tests.PrintCurrentTest(t)()
+
+                       req = NewRequest(t, "GET", url+"/filelists.xml.gz")
+                       resp := MakeRequest(t, req, http.StatusOK)
+
+                       type Filelists struct {
+                               XMLName      xml.Name `xml:"filelists"`
+                               Xmlns        string   `xml:"xmlns,attr"`
+                               PackageCount int      `xml:"packages,attr"`
+                               Packages     []struct {
+                                       Pkgid        string `xml:"pkgid,attr"`
+                                       Name         string `xml:"name,attr"`
+                                       Architecture string `xml:"arch,attr"`
+                                       Version      struct {
+                                               Epoch   string `xml:"epoch,attr"`
+                                               Version string `xml:"ver,attr"`
+                                               Release string `xml:"rel,attr"`
+                                       } `xml:"version"`
+                                       Files []*rpm_module.File `xml:"file"`
+                               } `xml:"package"`
+                       }
+
+                       var result Filelists
+                       decodeGzipXML(t, resp, &result)
+
+                       assert.EqualValues(t, 1, result.PackageCount)
+                       assert.Len(t, result.Packages, 1)
+                       p := result.Packages[0]
+                       assert.NotEmpty(t, p.Pkgid)
+                       assert.Equal(t, packageName, p.Name)
+                       assert.Equal(t, packageArchitecture, p.Architecture)
+                       assert.Len(t, p.Files, 1)
+                       f := p.Files[0]
+                       assert.Equal(t, "/usr/local/bin/hello", f.Path)
+               })
+
+               t.Run("other.xml.gz", func(t *testing.T) {
+                       defer tests.PrintCurrentTest(t)()
+
+                       req = NewRequest(t, "GET", url+"/other.xml.gz")
+                       resp := MakeRequest(t, req, http.StatusOK)
+
+                       type Other struct {
+                               XMLName      xml.Name `xml:"otherdata"`
+                               Xmlns        string   `xml:"xmlns,attr"`
+                               PackageCount int      `xml:"packages,attr"`
+                               Packages     []struct {
+                                       Pkgid        string `xml:"pkgid,attr"`
+                                       Name         string `xml:"name,attr"`
+                                       Architecture string `xml:"arch,attr"`
+                                       Version      struct {
+                                               Epoch   string `xml:"epoch,attr"`
+                                               Version string `xml:"ver,attr"`
+                                               Release string `xml:"rel,attr"`
+                                       } `xml:"version"`
+                                       Changelogs []*rpm_module.Changelog `xml:"changelog"`
+                               } `xml:"package"`
+                       }
+
+                       var result Other
+                       decodeGzipXML(t, resp, &result)
+
+                       assert.EqualValues(t, 1, result.PackageCount)
+                       assert.Len(t, result.Packages, 1)
+                       p := result.Packages[0]
+                       assert.NotEmpty(t, p.Pkgid)
+                       assert.Equal(t, packageName, p.Name)
+                       assert.Equal(t, packageArchitecture, p.Architecture)
+                       assert.Len(t, p.Changelogs, 1)
+                       c := p.Changelogs[0]
+                       assert.Equal(t, "KN4CK3R <dummy@gitea.io>", c.Author)
+                       assert.EqualValues(t, 1678276800, c.Date)
+                       assert.Equal(t, "- Changelog message.", c.Text)
+               })
+       })
+
+       t.Run("Delete", func(t *testing.T) {
+               defer tests.PrintCurrentTest(t)()
+
+               req := NewRequest(t, "DELETE", fmt.Sprintf("%s/package/%s/%s/%s", rootURL, packageName, packageVersion, packageArchitecture))
+               MakeRequest(t, req, http.StatusUnauthorized)
+
+               req = NewRequest(t, "DELETE", fmt.Sprintf("%s/package/%s/%s/%s", rootURL, packageName, packageVersion, packageArchitecture))
+               req = AddBasicAuthHeader(req, user.Name)
+               MakeRequest(t, req, http.StatusNoContent)
+
+               pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeRpm)
+               assert.NoError(t, err)
+               assert.Empty(t, pvs)
+
+               req = NewRequest(t, "DELETE", fmt.Sprintf("%s/package/%s/%s/%s", rootURL, packageName, packageVersion, packageArchitecture))
+               req = AddBasicAuthHeader(req, user.Name)
+               MakeRequest(t, req, http.StatusNotFound)
+       })
+}
diff --git a/web_src/svg/gitea-rpm.svg b/web_src/svg/gitea-rpm.svg
new file mode 100644 (file)
index 0000000..eba9dd0
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="920" height="537.4" viewBox="0 0 640 409" xmlns="http://www.w3.org/2000/svg">
+<g transform="matrix(1.111 0 0 1.111 -37.67 -15.95)">
+<path d="m242.1 26.14c12.57-1.932 25.26-3.344 37.97-4.014 2.258-0.538 4.594-0.4281 6.901-0.5105 18.04-0.5753 36.1-0.5493 54.13 0.2629 32.54 2.301 64.92 7.707 96.31 16.72l-0.1329 0.1763c-4.602 6.224-9.218 12.44-13.73 18.73-1.352 0.2495-2.536-0.6058-3.74-1.036-23.82-9.827-49.17-15.53-74.68-18.62-41.97-4.298-84.94-0.8447-125.2 12.07-29.64 9.588-58.27 24.37-80.38 46.67-15.67 16.02-27.69 36.74-29.33 59.45-1.519 10.82 1.072 21.62 3.364 32.13l-99.42 17.53c-0.2403-17.54 4.337-34.9 11.62-50.77 14.49-30.9 39.62-55.59 67.61-74.49 18.93-12.71 39.39-23.08 60.65-31.3 28.32-11.07 57.98-18.63 88.06-22.99z" fill="#040404"/>
+<path d="m423.6 57.5c4.513-6.288 9.128-12.5 13.73-18.73 42.15 12.34 82.85 31.84 116.1 60.88 13.59 12.17 25.89 25.94 35.33 41.61 11.18 18.44 18.27 39.56 19.03 61.19-33.06-5.778-66.1-11.6-99.16-17.4 4.068-12.94 4.103-27.02 1.317-40.23-5.163-22.82-19.79-42.41-37.44-57.28-14.71-12.38-31.49-22.3-49.27-29.56z" fill="#d72123"/>
+<path d="m301.7 148.6c1.946-3.169 5.372-5.696 9.269-5.227 4.405 0.1127 7.799 3.752 8.711 7.872l310.8 55.28-1.038 5.206-310.7-54.48c-2.478 4.312-8.228 6.539-12.74 3.993-3.131-1.369-4.2-4.72-5.613-7.531l-19.13-3.065 0.8769-5.381z"/>
+<path d="m156.1 299.5 0.1743-77.35 32.9 0.0614c34.68 0.0648 42.48 0.5632 57.86 3.698 37.71 7.684 62.74 25.76 67.65 48.85 4.979 23.41-13.56 44.79-51.04 58.88-13.48 5.067-31.77 9.041-46.31 10.07l-6.231 0.4388-0.373 24.86-2.212 0.3956c-1.217 0.2176-12.54 1.804-25.17 3.526s-23.96 3.307-25.19 3.524l-2.23 0.3938zm60.01 29.84c1.769-0.4157 6.3-2.234 10.07-4.041 14.18-6.795 24.53-16.45 30.1-28.09 4.309-8.991 4.629-17.43 1.027-27.06-4.125-11.03-15.91-23.02-29.19-29.72-4.374-2.206-13.08-4.908-15.8-4.908h-1.556v47.29c0 42.35 0.1115 47.29 1.067 47.29 0.5869 0 2.514-0.3402 4.283-0.7559zm-202-46.87v-60.65h54.83v1.456c0 1.28 0.2694 1.401 2.227 0.9981 12.87-2.647 32.27-3.843 40.59-2.5 2.889 0.466 7.351 1.561 9.916 2.433 5.431 1.847 15.09 6.023 15.08 6.522-3e-3 0.1885-2.541 1.86-5.641 3.714s-9.715 5.957-14.7 9.117l-9.064 5.745-2.566-1.744c-5.163-3.508-10.69-4.839-20.08-4.839-4.957 0-10.08 0.3898-12.16 0.925l-3.598 0.925v98.55h-54.83zm337.2-0.0456v-60.7l107.4 0.2996c109 0.304 112.3 0.3808 126.2 2.928 10.41 1.908 16.53 4.219 19.79 7.479l2.913 2.911v107.8h-54.83v-106.1l-2.913-1.252c-2.549-1.096-5.417-1.283-22.96-1.499l-20.05-0.2471v109.1h-54.82l-0.3511-106.4-2.741-1.055c-2.177-0.838-6.871-1.11-22.79-1.323l-20.05-0.2672v109.1h-54.83z" fill="#040404" stroke-width=".6853"/>
+</g>
+</svg>
\ No newline at end of file