├── .coveragerc ├── .dockerignore ├── .editorconfig ├── .env ├── .git-blame-ignore-revs ├── .gitignore ├── .gitlab-ci.yml ├── .gitlab └── issue_templates │ ├── Bug.md │ └── Feature.md ├── .mailmap ├── .pre-commit-config.yaml ├── .tx └── config ├── AUTHORS ├── CONTRIBUTING.md ├── COPYING ├── Dockerfile ├── INSTALL ├── LICENSES └── starlette_exporter ├── README.md ├── TESTING ├── alembic.ini ├── aurweb ├── __init__.py ├── archives │ ├── __init__.py │ └── spec │ │ ├── __init__.py │ │ ├── base.py │ │ ├── metadata.py │ │ ├── pkgbases.py │ │ ├── pkgnames.py │ │ └── users.py ├── asgi.py ├── aur_logging.py ├── aur_redis.py ├── auth │ ├── __init__.py │ └── creds.py ├── benchmark.py ├── cache.py ├── captcha.py ├── config.py ├── cookies.py ├── db.py ├── defaults.py ├── exceptions.py ├── filters.py ├── git │ ├── __init__.py │ ├── auth.py │ ├── serve.py │ └── update.py ├── initdb.py ├── l10n.py ├── models │ ├── __init__.py │ ├── accepted_term.py │ ├── account_type.py │ ├── api_rate_limit.py │ ├── ban.py │ ├── declarative.py │ ├── dependency_type.py │ ├── group.py │ ├── license.py │ ├── official_provider.py │ ├── package.py │ ├── package_base.py │ ├── package_blacklist.py │ ├── package_comaintainer.py │ ├── package_comment.py │ ├── package_dependency.py │ ├── package_group.py │ ├── package_keyword.py │ ├── package_license.py │ ├── package_notification.py │ ├── package_relation.py │ ├── package_request.py │ ├── package_source.py │ ├── package_vote.py │ ├── relation_type.py │ ├── request_type.py │ ├── session.py │ ├── ssh_pub_key.py │ ├── term.py │ ├── user.py │ ├── vote.py │ └── voteinfo.py ├── packages │ ├── __init__.py │ ├── requests.py │ ├── search.py │ └── util.py ├── pkgbase │ ├── __init__.py │ ├── actions.py │ ├── util.py │ └── validate.py ├── prometheus.py ├── ratelimit.py ├── requests │ ├── __init__.py │ └── util.py ├── routers │ ├── __init__.py │ ├── accounts.py │ ├── auth.py │ ├── html.py │ ├── package_maintainer.py │ ├── packages.py │ ├── pkgbase.py │ ├── requests.py │ ├── rpc.py │ ├── rss.py │ └── sso.py ├── rpc.py ├── schema.py ├── scripts │ ├── __init__.py │ ├── adduser.py │ ├── aurblup.py │ ├── config.py │ ├── git_archive.py │ ├── mkpkglists.py │ ├── notify.py │ ├── pkgmaint.py │ ├── popupdate.py │ ├── rendercomment.py │ ├── usermaint.py │ └── votereminder.py ├── spawn.py ├── statistics.py ├── templates.py ├── testing │ ├── __init__.py │ ├── alpm.py │ ├── email.py │ ├── filelock.py │ ├── git.py │ ├── html.py │ ├── prometheus.py │ ├── requests.py │ └── smtp.py ├── time.py ├── users │ ├── __init__.py │ ├── update.py │ ├── util.py │ └── validate.py └── util.py ├── cache └── .gitkeep ├── ci └── tf │ ├── .terraform.lock.hcl │ ├── main.tf │ ├── terraform.tfvars │ ├── variables.tf │ └── versions.tf ├── cliff.toml ├── conf ├── cgitrc.proto ├── config.defaults ├── config.dev ├── fcgiwrap.service.proto └── fcgiwrap.socket.proto ├── doc ├── Makefile ├── docker.md ├── git-archive.md ├── git-interface.txt ├── i18n.md ├── maintenance.txt ├── repos │ ├── metadata-repo.md │ ├── pkgbases-repo.md │ ├── pkgnames-repo.md │ └── users-repo.md ├── rpc.txt ├── specs │ ├── metadata.md │ ├── pkgbases.md │ ├── pkgnames.md │ ├── popularity.md │ └── users.md ├── sso.txt └── web-auth.md ├── docker-compose.aur-dev.yml ├── docker-compose.override.yml ├── docker-compose.yml ├── docker ├── README.md ├── ca-entrypoint.sh ├── cgit-entrypoint.sh ├── config │ ├── aurweb-cron │ ├── grafana │ │ └── datasources │ │ │ └── datasource.yml │ ├── nginx.conf │ ├── prometheus.yml │ └── tempo.yml ├── cron-entrypoint.sh ├── fastapi-entrypoint.sh ├── git-entrypoint.sh ├── health │ ├── ca.sh │ ├── cgit.sh │ ├── fastapi.sh │ ├── mariadb.sh │ ├── nginx.sh │ ├── prometheus.sh │ ├── redis.sh │ ├── smartgit.sh │ ├── sshd.sh │ └── tempo.sh ├── hypercorn.env ├── localhost.ext ├── logging.conf ├── mariadb-entrypoint.sh ├── mariadb-init-entrypoint.sh ├── nginx-entrypoint.sh ├── redis-entrypoint.sh ├── scripts │ ├── install-deps.sh │ ├── install-python-deps.sh │ ├── run-ca.sh │ ├── run-cgit.sh │ ├── run-cron.sh │ ├── run-fastapi.sh │ ├── run-nginx.sh │ ├── run-pytests.sh │ ├── run-redis.sh │ ├── run-sharness.sh │ ├── run-smartgit.sh │ ├── run-sshd.sh │ ├── run-tests.sh │ └── update-step-config ├── sharness-entrypoint.sh ├── smartgit-entrypoint.sh ├── test-mysql-entrypoint.sh └── tests-entrypoint.sh ├── examples ├── aurweb-git-auth.sh ├── aurweb-git-serve.sh ├── aurweb-git-update.sh ├── aurweb.service └── jsonp.html ├── gunicorn.conf.py ├── logging.conf ├── logging.prod.conf ├── logging.test.conf ├── logs └── .gitkeep ├── migrations ├── README ├── env.py ├── script.py.mako └── versions │ ├── 38e5b9982eea_add_indicies_on_packagebases_for_rss_.py │ ├── 56e2ce8e2ffa_utf8mb4_charset_and_collation.py │ ├── 6441d3b65270_add_popularityupdated_to_packagebase.py │ ├── 6a64dd126029_rename_tu_to_package_maintainer.py │ ├── 7d65d35fae45_rename_tu_tables_columns.py │ ├── 9e3158957fd7_add_packagekeyword_packagebaseuid.py │ ├── be7adae47ac3_upgrade_voteinfo_integers.py │ ├── c5a6a9b661a0_add_index_on_packagebases_popularity_.py │ ├── d64e5571bc8d_fix_pkgvote_votets.py │ ├── e4e49ffce091_add_hidedeletedcomments_to_user.py │ ├── ef39fcd6e1cd_add_sso_account_id_in_table_users.py │ └── f47cad5d6d03_initial_revision.py ├── po ├── Makefile ├── ar.po ├── ast.po ├── aurweb.pot ├── az.po ├── az_AZ.po ├── bg.po ├── ca.po ├── ca_ES.po ├── cs.po ├── da.po ├── de.po ├── el.po ├── es.po ├── es_419.po ├── et.po ├── fi.po ├── fi_FI.po ├── fr.po ├── he.po ├── hi_IN.po ├── hr.po ├── hu.po ├── id.po ├── id_ID.po ├── is.po ├── it.po ├── ja.po ├── ko.po ├── lt.po ├── nb.po ├── nb_NO.po ├── nl.po ├── pl.po ├── pt.po ├── pt_BR.po ├── pt_PT.po ├── ro.po ├── ru.po ├── sk.po ├── sr.po ├── sr_RS.po ├── sv_SE.po ├── tr.po ├── uk.po ├── vi.po ├── zh.po ├── zh_CN.po └── zh_TW.po ├── poetry.lock ├── pyproject.toml ├── pytest.ini ├── renovate.json ├── schema └── gendummydata.py ├── setup.cfg ├── static ├── css │ ├── archnavbar │ │ ├── archlogo.png │ │ ├── archnavbar.css │ │ └── aurlogo.png │ ├── archweb.css │ ├── aurweb.css │ └── cgit.css ├── html │ └── cgit │ │ ├── footer.html │ │ └── header.html ├── images │ ├── ICON-LICENSE │ ├── action-undo.min.svg │ ├── action-undo.svg │ ├── ajax-loader.gif │ ├── favicon.ico │ ├── pencil.min.svg │ ├── pencil.svg │ ├── pin.min.svg │ ├── pin.svg │ ├── rss.svg │ ├── unpin.min.svg │ ├── unpin.svg │ ├── x.min.svg │ └── x.svg └── js │ ├── comment-edit.js │ ├── copy.js │ ├── typeahead-home.js │ ├── typeahead-pkgbase-merge.js │ ├── typeahead-pkgbase-request.js │ ├── typeahead-requests.js │ └── typeahead.js ├── templates ├── account │ ├── comments.html │ ├── delete.html │ ├── edit.html │ ├── index.html │ ├── search.html │ └── show.html ├── addvote.html ├── dashboard.html ├── errors │ ├── 404.html │ ├── 500.html │ ├── 503.html │ └── detail.html ├── home.html ├── index.html ├── login.html ├── package-maintainer │ ├── index.html │ └── show.html ├── packages │ ├── index.html │ └── show.html ├── partials │ ├── account │ │ ├── comment.html │ │ └── results.html │ ├── account_form.html │ ├── archdev-navbar.html │ ├── body.html │ ├── comment_actions.html │ ├── comment_content.html │ ├── error.html │ ├── footer.html │ ├── head.html │ ├── layout.html │ ├── meta.html │ ├── navbar.html │ ├── package-maintainer │ │ ├── last_votes.html │ │ ├── proposal │ │ │ ├── details.html │ │ │ ├── form.html │ │ │ └── voters.html │ │ └── proposals.html │ ├── packages │ │ ├── actions.html │ │ ├── comment.html │ │ ├── comment_form.html │ │ ├── comments.html │ │ ├── details.html │ │ ├── package_metadata.html │ │ ├── pkgbase_metadata.html │ │ ├── requests.html │ │ ├── results.html │ │ ├── search.html │ │ ├── search_actions.html │ │ ├── search_results.html │ │ ├── search_widget.html │ │ ├── statistics.html │ │ └── updates.html │ ├── pager.html │ ├── set_lang.html │ ├── statistics.html │ └── support.html ├── passreset.html ├── pkgbase │ ├── comaintainers.html │ ├── comments │ │ └── edit.html │ ├── delete.html │ ├── disown.html │ ├── flag-comment.html │ ├── flag.html │ ├── index.html │ ├── merge.html │ ├── request.html │ └── voters.html ├── register.html ├── requests.html ├── requests │ └── close.html ├── testing │ ├── PKGBUILD.j2 │ ├── SRCINFO.j2 │ └── alpm_package.j2 └── tos │ └── index.html ├── test ├── Makefile ├── README.md ├── __init__.py ├── conftest.py ├── scripts │ └── cover ├── setup.sh ├── sharness.sh ├── t1100-git-auth.t ├── t1200-git-serve.t ├── t1300-git-update.t ├── test_accepted_term.py ├── test_account_type.py ├── test_accounts_routes.py ├── test_adduser.py ├── test_api_rate_limit.py ├── test_asgi.py ├── test_aurblup.py ├── test_auth.py ├── test_auth_routes.py ├── test_ban.py ├── test_cache.py ├── test_captcha.py ├── test_config.py ├── test_cookies.py ├── test_db.py ├── test_defaults.py ├── test_dependency_type.py ├── test_email.py ├── test_exceptions.py ├── test_filelock.py ├── test_filters.py ├── test_git_archives.py ├── test_git_update.py ├── test_group.py ├── test_homepage.py ├── test_html.py ├── test_initdb.py ├── test_l10n.py ├── test_license.py ├── test_logging.py ├── test_metrics.py ├── test_mkpkglists.py ├── test_notify.py ├── test_official_provider.py ├── test_package.py ├── test_package_base.py ├── test_package_blacklist.py ├── test_package_comaintainer.py ├── test_package_comment.py ├── test_package_dependency.py ├── test_package_group.py ├── test_package_keyword.py ├── test_package_license.py ├── test_package_maintainer_routes.py ├── test_package_notification.py ├── test_package_relation.py ├── test_package_request.py ├── test_package_source.py ├── test_package_vote.py ├── test_packages_routes.py ├── test_packages_util.py ├── test_pkgbase_routes.py ├── test_pkgmaint.py ├── test_pm_vote.py ├── test_popupdate.py ├── test_ratelimit.py ├── test_redis.py ├── test_relation_type.py ├── test_rendercomment.py ├── test_request_type.py ├── test_requests.py ├── test_routes.py ├── test_rpc.py ├── test_rss.py ├── test_session.py ├── test_spawn.py ├── test_ssh_pub_key.py ├── test_statistics.py ├── test_templates.py ├── test_term.py ├── test_time.py ├── test_user.py ├── test_usermaint.py ├── test_util.py ├── test_voteinfo.py └── test_votereminder.py ├── upgrading ├── 1.2.10.txt ├── 1.3.0.txt ├── 1.5.2.txt ├── 1.5.3.txt ├── 1.7.0.txt ├── 1.8.0.txt ├── 1.8.1.txt ├── 1.8.2.txt ├── 1.9.0.txt ├── 1.9.1.txt ├── 2.0.0.txt ├── 2.1.0.txt ├── 2.2.0.txt ├── 2.3.0.txt ├── 3.0.0.txt ├── 3.1.0.txt ├── 3.2.0.txt ├── 3.4.0.txt ├── 3.5.0.txt ├── 4.0.0.txt ├── 4.1.0.txt ├── 4.2.0.txt ├── 4.2.1.txt ├── 4.3.0.txt ├── 4.4.0.txt ├── 4.4.1.txt ├── 4.5.0.txt ├── 4.6.0.txt ├── 4.7.0.txt ├── 5.x.x.txt ├── longerpkgname.txt └── post-checkout └── util ├── fix-coverage └── sendmail /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | disable_warnings = already-imported 3 | 4 | [report] 5 | include = aurweb/* 6 | fail_under = 95 7 | exclude_lines = 8 | if __name__ == .__main__.: 9 | pragma: no cover 10 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Config files 2 | conf/config 3 | conf/config.sqlite 4 | conf/config.sqlite.defaults 5 | conf/docker 6 | conf/docker.defaults 7 | 8 | # Compiled translation files 9 | **/*.mo 10 | 11 | # Typical virtualenv directories 12 | env/ 13 | venv/ 14 | .venv/ 15 | 16 | # Test output 17 | htmlcov/ 18 | test-emails/ 19 | test/__pycache__ 20 | test/test-results 21 | test/trash_directory* 22 | .coverage 23 | .pytest_cache 24 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig configuration for aurweb 2 | # https://editorconfig.org 3 | 4 | # Top-most EditorConfig file 5 | root = true 6 | 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | charset = utf-8 11 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | FASTAPI_BACKEND="uvicorn" 2 | FASTAPI_WORKERS=2 3 | MARIADB_SOCKET_DIR="/var/run/mysqld/" 4 | AURWEB_FASTAPI_PREFIX=https://localhost:8444 5 | AURWEB_SSHD_PREFIX=ssh://aur@localhost:2222 6 | GIT_DATA_DIR="./aur.git/" 7 | TEST_RECURSION_LIMIT=10000 8 | COMMIT_HASH= 9 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # style: Run pre-commit 2 | 9c6c13b78a30cb9d800043410799e29631f803d2 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /data/ 2 | __pycache__/ 3 | *.py[cod] 4 | .vim/ 5 | .pylintrc 6 | .coverage 7 | .idea 8 | /cache/* 9 | /logs/* 10 | /build/ 11 | /dist/ 12 | /aurweb.egg-info/ 13 | /personal/ 14 | /notes/ 15 | /vendor/ 16 | /pyrightconfig.json 17 | /taskell.md 18 | aur.git/ 19 | aurweb.sqlite3 20 | conf/config 21 | conf/config.sqlite 22 | conf/config.sqlite.defaults 23 | conf/docker 24 | conf/docker.defaults 25 | data.sql 26 | dummy-data.sql* 27 | fastapi_aw/ 28 | htmlcov/ 29 | po/*.mo 30 | po/*.po~ 31 | po/POTFILES 32 | schema/aur-schema-sqlite.sql 33 | test/test-results/ 34 | test/trash_directory* 35 | web/locale/*/ 36 | web/html/*.gz 37 | 38 | # Do not stage compiled asciidoc: make -C doc 39 | doc/rpc.html 40 | 41 | # Ignore any user-configured .envrc files at the root. 42 | /.envrc 43 | 44 | # Ignore .python-version file from Pyenv 45 | .python-version 46 | 47 | # Ignore coverage report 48 | coverage.xml 49 | 50 | # Ignore pytest report 51 | report.xml 52 | 53 | # Ignore test emails 54 | test-emails/ 55 | 56 | # Ignore typical virtualenv directories 57 | env/ 58 | venv/ 59 | .venv/ 60 | 61 | # Ignore some terraform files 62 | /ci/tf/.terraform 63 | /ci/tf/terraform.tfstate* 64 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | Andrea Scarpino 2 | Cilyan Olowen 3 | Denis Kobozev 4 | eliott 5 | Giovanni Scafora 6 | Jason Chu 7 | Laszlo Papp 8 | Lukas Fleischer 9 | Mod Gao 10 | Mod Gao 11 | Paul Mattal 12 | Paul Mattal 13 | Paul Mattal 14 | Sergej Pupykin 15 | Simo Leone 16 | Thayer Williams 17 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.5.0 4 | hooks: 5 | - id: check-added-large-files 6 | - id: check-case-conflict 7 | - id: check-merge-conflict 8 | - id: check-toml 9 | - id: end-of-file-fixer 10 | - id: trailing-whitespace 11 | exclude: ^po/ 12 | - id: debug-statements 13 | 14 | - repo: https://github.com/myint/autoflake 15 | rev: v2.3.1 16 | hooks: 17 | - id: autoflake 18 | args: 19 | - --in-place 20 | - --remove-all-unused-imports 21 | - --ignore-init-module-imports 22 | 23 | - repo: https://github.com/pycqa/isort 24 | rev: 5.13.2 25 | hooks: 26 | - id: isort 27 | 28 | - repo: https://github.com/psf/black 29 | rev: 24.4.1 30 | hooks: 31 | - id: black 32 | 33 | - repo: https://github.com/PyCQA/flake8 34 | rev: 7.0.0 35 | hooks: 36 | - id: flake8 37 | -------------------------------------------------------------------------------- /.tx/config: -------------------------------------------------------------------------------- 1 | [main] 2 | host = https://app.transifex.com 3 | 4 | [o:lfleischer:p:aurweb:r:aurwebpot] 5 | file_filter = po/.po 6 | source_file = po/aurweb.pot 7 | source_lang = en 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM archlinux:base-devel 2 | 3 | VOLUME /root/.cache/pypoetry/cache 4 | VOLUME /root/.cache/pypoetry/artifacts 5 | VOLUME /root/.cache/pre-commit 6 | 7 | ENV PATH="/root/.poetry/bin:${PATH}" 8 | ENV PYTHONPATH=/aurweb 9 | ENV AUR_CONFIG=conf/config 10 | ENV COMPOSE=1 11 | 12 | # Install system-wide dependencies. 13 | COPY ./docker/scripts/install-deps.sh /install-deps.sh 14 | RUN /install-deps.sh 15 | 16 | # Copy Docker scripts 17 | COPY ./docker /docker 18 | COPY ./docker/scripts/* /usr/local/bin/ 19 | 20 | 21 | # Copy over all aurweb files. 22 | COPY . /aurweb 23 | 24 | # Working directory is aurweb root @ /aurweb. 25 | WORKDIR /aurweb 26 | 27 | # Copy initial config to conf/config. 28 | RUN cp -vf conf/config.dev conf/config 29 | RUN sed -i "s;YOUR_AUR_ROOT;/aurweb;g" conf/config 30 | 31 | # Install Python dependencies. 32 | RUN /docker/scripts/install-python-deps.sh compose 33 | 34 | # Compile asciidocs. 35 | RUN make -C doc 36 | 37 | # Add our aur user. 38 | RUN useradd -U -d /aurweb -c 'AUR User' aur 39 | 40 | # Setup some default system stuff. 41 | RUN ln -sf /usr/share/zoneinfo/UTC /etc/localtime 42 | 43 | # Install translations. 44 | RUN make -C po all install 45 | 46 | # Install pre-commit repositories and run lint check. 47 | RUN pre-commit run -a 48 | -------------------------------------------------------------------------------- /aurweb/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/aurweb/ae432337466c2e38a38eaa2fdb54f193cf6a958c/aurweb/__init__.py -------------------------------------------------------------------------------- /aurweb/archives/__init__.py: -------------------------------------------------------------------------------- 1 | # aurweb.archives 2 | -------------------------------------------------------------------------------- /aurweb/archives/spec/__init__.py: -------------------------------------------------------------------------------- 1 | # aurweb.archives.spec 2 | -------------------------------------------------------------------------------- /aurweb/archives/spec/pkgbases.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable 2 | 3 | import orjson 4 | 5 | from aurweb import config, db 6 | from aurweb.models import PackageBase 7 | 8 | from .base import GitInfo, SpecBase, SpecOutput 9 | 10 | ORJSON_OPTS = orjson.OPT_SORT_KEYS | orjson.OPT_INDENT_2 11 | 12 | 13 | class Spec(SpecBase): 14 | def __init__(self) -> "Spec": 15 | self.pkgbases_repo = GitInfo(config.get("git-archive", "pkgbases-repo")) 16 | 17 | def generate(self) -> Iterable[SpecOutput]: 18 | query = db.query(PackageBase.Name).order_by(PackageBase.Name.asc()).all() 19 | pkgbases = [pkgbase.Name for pkgbase in query] 20 | 21 | self.add_output( 22 | "pkgbase.json", 23 | self.pkgbases_repo, 24 | orjson.dumps(pkgbases, option=ORJSON_OPTS), 25 | ) 26 | return self.outputs 27 | -------------------------------------------------------------------------------- /aurweb/archives/spec/pkgnames.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable 2 | 3 | import orjson 4 | 5 | from aurweb import config, db 6 | from aurweb.models import Package, PackageBase 7 | 8 | from .base import GitInfo, SpecBase, SpecOutput 9 | 10 | ORJSON_OPTS = orjson.OPT_SORT_KEYS | orjson.OPT_INDENT_2 11 | 12 | 13 | class Spec(SpecBase): 14 | def __init__(self) -> "Spec": 15 | self.pkgnames_repo = GitInfo(config.get("git-archive", "pkgnames-repo")) 16 | 17 | def generate(self) -> Iterable[SpecOutput]: 18 | query = ( 19 | db.query(Package.Name) 20 | .join(PackageBase, PackageBase.ID == Package.PackageBaseID) 21 | .order_by(Package.Name.asc()) 22 | .all() 23 | ) 24 | pkgnames = [pkg.Name for pkg in query] 25 | 26 | self.add_output( 27 | "pkgname.json", 28 | self.pkgnames_repo, 29 | orjson.dumps(pkgnames, option=ORJSON_OPTS), 30 | ) 31 | return self.outputs 32 | -------------------------------------------------------------------------------- /aurweb/archives/spec/users.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable 2 | 3 | import orjson 4 | 5 | from aurweb import config, db 6 | from aurweb.models import User 7 | 8 | from .base import GitInfo, SpecBase, SpecOutput 9 | 10 | ORJSON_OPTS = orjson.OPT_SORT_KEYS | orjson.OPT_INDENT_2 11 | 12 | 13 | class Spec(SpecBase): 14 | def __init__(self) -> "Spec": 15 | self.users_repo = GitInfo(config.get("git-archive", "users-repo")) 16 | 17 | def generate(self) -> Iterable[SpecOutput]: 18 | query = db.query(User.Username).order_by(User.Username.asc()).all() 19 | users = [user.Username for user in query] 20 | 21 | self.add_output( 22 | "users.json", 23 | self.users_repo, 24 | orjson.dumps(users, option=ORJSON_OPTS), 25 | ) 26 | return self.outputs 27 | -------------------------------------------------------------------------------- /aurweb/aur_logging.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import logging.config 3 | import os 4 | 5 | import aurweb.config 6 | 7 | # For testing, users should set LOG_CONFIG=logging.test.conf 8 | # We test against various debug log output. 9 | aurwebdir = aurweb.config.get("options", "aurwebdir") 10 | log_config = os.environ.get("LOG_CONFIG", "logging.conf") 11 | config_path = os.path.join(aurwebdir, log_config) 12 | 13 | logging.config.fileConfig(config_path, disable_existing_loggers=False) 14 | logging.getLogger("root").addHandler(logging.NullHandler()) 15 | 16 | 17 | def get_logger(name: str) -> logging.Logger: 18 | """A logging.getLogger wrapper. Importing this function and 19 | using it to get a module-local logger ensures that logging.conf 20 | initialization is performed wherever loggers are used. 21 | 22 | :param name: Logger name; typically `__name__` 23 | :returns: name's logging.Logger 24 | """ 25 | 26 | return logging.getLogger(name) 27 | -------------------------------------------------------------------------------- /aurweb/benchmark.py: -------------------------------------------------------------------------------- 1 | from datetime import UTC, datetime 2 | 3 | 4 | class Benchmark: 5 | def __init__(self): 6 | self.start() 7 | 8 | def _timestamp(self) -> float: 9 | """Generate a timestamp.""" 10 | return float(datetime.now(UTC).timestamp()) 11 | 12 | def start(self) -> int: 13 | """Start a benchmark.""" 14 | self.current = self._timestamp() 15 | return self.current 16 | 17 | def end(self): 18 | """Return the diff between now - start().""" 19 | n = self._timestamp() - self.current 20 | self.current = float(0) 21 | return n 22 | -------------------------------------------------------------------------------- /aurweb/cookies.py: -------------------------------------------------------------------------------- 1 | def samesite() -> str: 2 | """Produce cookie SameSite value. 3 | 4 | Currently this is hard-coded to return "lax" 5 | 6 | :returns "lax" 7 | """ 8 | return "lax" 9 | -------------------------------------------------------------------------------- /aurweb/defaults.py: -------------------------------------------------------------------------------- 1 | """Constant default values centralized in one place.""" 2 | 3 | # Default [O]ffset 4 | O = 0 5 | 6 | # Default [P]er [P]age 7 | PP = 50 8 | 9 | # Default Comments Per Page 10 | COMMENTS_PER_PAGE = 10 11 | 12 | # A whitelist of valid PP values 13 | PP_WHITELIST = {50, 100, 250} 14 | 15 | # Default `by` parameter for RPC search. 16 | RPC_SEARCH_BY = "name-desc" 17 | 18 | 19 | def fallback_pp(per_page: int) -> int: 20 | """If `per_page` is a valid value in PP_WHITELIST, return it. 21 | Otherwise, return defaults.PP.""" 22 | if per_page not in PP_WHITELIST: 23 | return PP 24 | return per_page 25 | -------------------------------------------------------------------------------- /aurweb/git/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/aurweb/ae432337466c2e38a38eaa2fdb54f193cf6a958c/aurweb/git/__init__.py -------------------------------------------------------------------------------- /aurweb/models/__init__.py: -------------------------------------------------------------------------------- 1 | """Collection of all aurweb SQLAlchemy declarative models.""" 2 | 3 | from .accepted_term import AcceptedTerm # noqa: F401 4 | from .account_type import AccountType # noqa: F401 5 | from .api_rate_limit import ApiRateLimit # noqa: F401 6 | from .ban import Ban # noqa: F401 7 | from .dependency_type import DependencyType # noqa: F401 8 | from .group import Group # noqa: F401 9 | from .license import License # noqa: F401 10 | from .official_provider import OfficialProvider # noqa: F401 11 | from .package import Package # noqa: F401 12 | from .package_base import PackageBase # noqa: F401 13 | from .package_blacklist import PackageBlacklist # noqa: F401 14 | from .package_comaintainer import PackageComaintainer # noqa: F401 15 | from .package_comment import PackageComment # noqa: F401 16 | from .package_dependency import PackageDependency # noqa: F401 17 | from .package_group import PackageGroup # noqa: F401 18 | from .package_keyword import PackageKeyword # noqa: F401 19 | from .package_license import PackageLicense # noqa: F401 20 | from .package_notification import PackageNotification # noqa: F401 21 | from .package_relation import PackageRelation # noqa: F401 22 | from .package_request import PackageRequest # noqa: F401 23 | from .package_source import PackageSource # noqa: F401 24 | from .package_vote import PackageVote # noqa: F401 25 | from .relation_type import RelationType # noqa: F401 26 | from .request_type import RequestType # noqa: F401 27 | from .session import Session # noqa: F401 28 | from .ssh_pub_key import SSHPubKey # noqa: F401 29 | from .term import Term # noqa: F401 30 | from .user import User # noqa: F401 31 | from .vote import Vote # noqa: F401 32 | from .voteinfo import VoteInfo # noqa: F401 33 | -------------------------------------------------------------------------------- /aurweb/models/accepted_term.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.exc import IntegrityError 2 | from sqlalchemy.orm import backref, relationship 3 | 4 | from aurweb import schema 5 | from aurweb.models.declarative import Base 6 | from aurweb.models.term import Term as _Term 7 | from aurweb.models.user import User as _User 8 | 9 | 10 | class AcceptedTerm(Base): 11 | __table__ = schema.AcceptedTerms 12 | __tablename__ = __table__.name 13 | __mapper_args__ = {"primary_key": [__table__.c.TermsID]} 14 | 15 | User = relationship( 16 | _User, 17 | backref=backref("accepted_terms", lazy="dynamic"), 18 | foreign_keys=[__table__.c.UsersID], 19 | ) 20 | 21 | Term = relationship( 22 | _Term, 23 | backref=backref("accepted_terms", lazy="dynamic"), 24 | foreign_keys=[__table__.c.TermsID], 25 | ) 26 | 27 | def __init__(self, **kwargs): 28 | super().__init__(**kwargs) 29 | 30 | if not self.User and not self.UsersID: 31 | raise IntegrityError( 32 | statement="Foreign key UsersID cannot be null.", 33 | orig="AcceptedTerms.UserID", 34 | params=("NULL"), 35 | ) 36 | 37 | if not self.Term and not self.TermsID: 38 | raise IntegrityError( 39 | statement="Foreign key TermID cannot be null.", 40 | orig="AcceptedTerms.TermID", 41 | params=("NULL"), 42 | ) 43 | -------------------------------------------------------------------------------- /aurweb/models/account_type.py: -------------------------------------------------------------------------------- 1 | from aurweb import schema 2 | from aurweb.models.declarative import Base 3 | 4 | USER = "User" 5 | PACKAGE_MAINTAINER = "Package Maintainer" 6 | DEVELOPER = "Developer" 7 | PACKAGE_MAINTAINER_AND_DEV = "Package Maintainer & Developer" 8 | 9 | USER_ID = 1 10 | PACKAGE_MAINTAINER_ID = 2 11 | DEVELOPER_ID = 3 12 | PACKAGE_MAINTAINER_AND_DEV_ID = 4 13 | 14 | # Map string constants to integer constants. 15 | ACCOUNT_TYPE_ID = { 16 | USER: USER_ID, 17 | PACKAGE_MAINTAINER: PACKAGE_MAINTAINER_ID, 18 | DEVELOPER: DEVELOPER_ID, 19 | PACKAGE_MAINTAINER_AND_DEV: PACKAGE_MAINTAINER_AND_DEV_ID, 20 | } 21 | 22 | # Reversed ACCOUNT_TYPE_ID mapping. 23 | ACCOUNT_TYPE_NAME = {v: k for k, v in ACCOUNT_TYPE_ID.items()} 24 | 25 | 26 | class AccountType(Base): 27 | """An ORM model of a single AccountTypes record.""" 28 | 29 | __table__ = schema.AccountTypes 30 | __tablename__ = __table__.name 31 | __mapper_args__ = {"primary_key": [__table__.c.ID]} 32 | 33 | def __init__(self, **kwargs): 34 | self.AccountType = kwargs.pop("AccountType") 35 | 36 | def __str__(self): 37 | return str(self.AccountType) 38 | 39 | def __repr__(self): 40 | return "" % (self.ID, str(self)) 41 | -------------------------------------------------------------------------------- /aurweb/models/api_rate_limit.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.exc import IntegrityError 2 | 3 | from aurweb import schema 4 | from aurweb.models.declarative import Base 5 | 6 | 7 | class ApiRateLimit(Base): 8 | __table__ = schema.ApiRateLimit 9 | __tablename__ = __table__.name 10 | __mapper_args__ = {"primary_key": [__table__.c.IP]} 11 | 12 | def __init__(self, **kwargs): 13 | super().__init__(**kwargs) 14 | 15 | if self.Requests is None: 16 | raise IntegrityError( 17 | statement="Column Requests cannot be null.", 18 | orig="ApiRateLimit.Requests", 19 | params=("NULL"), 20 | ) 21 | 22 | if self.WindowStart is None: 23 | raise IntegrityError( 24 | statement="Column WindowStart cannot be null.", 25 | orig="ApiRateLimit.WindowStart", 26 | params=("NULL"), 27 | ) 28 | -------------------------------------------------------------------------------- /aurweb/models/ban.py: -------------------------------------------------------------------------------- 1 | from fastapi import Request 2 | 3 | from aurweb import db, schema 4 | from aurweb.models.declarative import Base 5 | from aurweb.util import get_client_ip 6 | 7 | 8 | class Ban(Base): 9 | __table__ = schema.Bans 10 | __tablename__ = __table__.name 11 | __mapper_args__ = {"primary_key": [__table__.c.IPAddress]} 12 | 13 | def __init__(self, **kwargs): 14 | super().__init__(**kwargs) 15 | 16 | 17 | def is_banned(request: Request): 18 | ip = get_client_ip(request) 19 | exists = db.query(Ban).filter(Ban.IPAddress == ip).exists() 20 | return db.query(exists).scalar() 21 | -------------------------------------------------------------------------------- /aurweb/models/declarative.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from sqlalchemy.ext.declarative import declarative_base 4 | 5 | from aurweb import util 6 | 7 | 8 | def to_dict(model): 9 | return {c.name: getattr(model, c.name) for c in model.__table__.columns} 10 | 11 | 12 | def to_json(model, indent: int = None): 13 | return json.dumps( 14 | {k: util.jsonify(v) for k, v in to_dict(model).items()}, indent=indent 15 | ) 16 | 17 | 18 | Base = declarative_base() 19 | 20 | # Setup __table_args__ applicable to every table. 21 | Base.__table_args__ = {"autoload": False, "extend_existing": True} 22 | 23 | # Setup Base.as_dict and Base.json. 24 | # 25 | # With this, declarative models can use .as_dict() or .json() 26 | # at any time to produce a dict and json out of table columns. 27 | # 28 | Base.as_dict = to_dict 29 | Base.json = to_json 30 | -------------------------------------------------------------------------------- /aurweb/models/dependency_type.py: -------------------------------------------------------------------------------- 1 | from aurweb import schema 2 | from aurweb.models.declarative import Base 3 | 4 | DEPENDS = "depends" 5 | MAKEDEPENDS = "makedepends" 6 | CHECKDEPENDS = "checkdepends" 7 | OPTDEPENDS = "optdepends" 8 | 9 | DEPENDS_ID = 1 10 | MAKEDEPENDS_ID = 2 11 | CHECKDEPENDS_ID = 3 12 | OPTDEPENDS_ID = 4 13 | 14 | 15 | class DependencyType(Base): 16 | __table__ = schema.DependencyTypes 17 | __tablename__ = __table__.name 18 | __mapper_args__ = {"primary_key": [__table__.c.ID]} 19 | 20 | def __init__(self, Name: str = None): 21 | self.Name = Name 22 | -------------------------------------------------------------------------------- /aurweb/models/group.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.exc import IntegrityError 2 | 3 | from aurweb import schema 4 | from aurweb.models.declarative import Base 5 | 6 | 7 | class Group(Base): 8 | __table__ = schema.Groups 9 | __tablename__ = __table__.name 10 | __mapper_args__ = {"primary_key": [__table__.c.ID]} 11 | 12 | def __init__(self, **kwargs): 13 | super().__init__(**kwargs) 14 | if self.Name is None: 15 | raise IntegrityError( 16 | statement="Column Name cannot be null.", 17 | orig="Groups.Name", 18 | params=("NULL"), 19 | ) 20 | -------------------------------------------------------------------------------- /aurweb/models/license.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.exc import IntegrityError 2 | 3 | from aurweb import schema 4 | from aurweb.models.declarative import Base 5 | 6 | 7 | class License(Base): 8 | __table__ = schema.Licenses 9 | __tablename__ = __table__.name 10 | __mapper_args__ = {"primary_key": [__table__.c.ID]} 11 | 12 | def __init__(self, **kwargs): 13 | super().__init__(**kwargs) 14 | 15 | if not self.Name: 16 | raise IntegrityError( 17 | statement="Column Name cannot be null.", 18 | orig="Licenses.Name", 19 | params=("NULL"), 20 | ) 21 | -------------------------------------------------------------------------------- /aurweb/models/official_provider.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.exc import IntegrityError 2 | 3 | from aurweb import schema 4 | from aurweb.models.declarative import Base 5 | 6 | OFFICIAL_BASE = "https://archlinux.org" 7 | 8 | 9 | class OfficialProvider(Base): 10 | __table__ = schema.OfficialProviders 11 | __tablename__ = __table__.name 12 | __mapper_args__ = {"primary_key": [__table__.c.ID]} 13 | 14 | # OfficialProvider instances are official packages. 15 | is_official = True 16 | 17 | def __init__(self, **kwargs): 18 | super().__init__(**kwargs) 19 | 20 | if not self.Name: 21 | raise IntegrityError( 22 | statement="Column Name cannot be null.", 23 | orig="OfficialProviders.Name", 24 | params=("NULL"), 25 | ) 26 | 27 | if not self.Repo: 28 | raise IntegrityError( 29 | statement="Column Repo cannot be null.", 30 | orig="OfficialProviders.Repo", 31 | params=("NULL"), 32 | ) 33 | 34 | if not self.Provides: 35 | raise IntegrityError( 36 | statement="Column Provides cannot be null.", 37 | orig="OfficialProviders.Provides", 38 | params=("NULL"), 39 | ) 40 | -------------------------------------------------------------------------------- /aurweb/models/package.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.exc import IntegrityError 2 | from sqlalchemy.orm import backref, relationship 3 | 4 | from aurweb import schema 5 | from aurweb.models.declarative import Base 6 | from aurweb.models.package_base import PackageBase as _PackageBase 7 | 8 | 9 | class Package(Base): 10 | __table__ = schema.Packages 11 | __tablename__ = __table__.name 12 | __mapper_args__ = {"primary_key": [__table__.c.ID]} 13 | 14 | PackageBase = relationship( 15 | _PackageBase, 16 | backref=backref("packages", lazy="dynamic", cascade="all, delete"), 17 | foreign_keys=[__table__.c.PackageBaseID], 18 | ) 19 | 20 | # No Package instances are official packages. 21 | is_official = False 22 | 23 | def __init__(self, **kwargs): 24 | super().__init__(**kwargs) 25 | 26 | if not self.PackageBase and not self.PackageBaseID: 27 | raise IntegrityError( 28 | statement="Foreign key PackageBaseID cannot be null.", 29 | orig="Packages.PackageBaseID", 30 | params=("NULL"), 31 | ) 32 | 33 | if self.Name is None: 34 | raise IntegrityError( 35 | statement="Column Name cannot be null.", 36 | orig="Packages.Name", 37 | params=("NULL"), 38 | ) 39 | -------------------------------------------------------------------------------- /aurweb/models/package_blacklist.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.exc import IntegrityError 2 | 3 | from aurweb import schema 4 | from aurweb.models.declarative import Base 5 | 6 | 7 | class PackageBlacklist(Base): 8 | __table__ = schema.PackageBlacklist 9 | __tablename__ = __table__.name 10 | __mapper_args__ = {"primary_key": [__table__.c.ID]} 11 | 12 | def __init__(self, **kwargs): 13 | super().__init__(**kwargs) 14 | 15 | if not self.Name: 16 | raise IntegrityError( 17 | statement="Column Name cannot be null.", 18 | orig="PackageBlacklist.Name", 19 | params=("NULL"), 20 | ) 21 | -------------------------------------------------------------------------------- /aurweb/models/package_group.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.exc import IntegrityError 2 | from sqlalchemy.orm import backref, relationship 3 | 4 | from aurweb import schema 5 | from aurweb.models.declarative import Base 6 | from aurweb.models.group import Group as _Group 7 | from aurweb.models.package import Package as _Package 8 | 9 | 10 | class PackageGroup(Base): 11 | __table__ = schema.PackageGroups 12 | __tablename__ = __table__.name 13 | __mapper_args__ = {"primary_key": [__table__.c.PackageID, __table__.c.GroupID]} 14 | 15 | Package = relationship( 16 | _Package, 17 | backref=backref("package_groups", lazy="dynamic", cascade="all, delete"), 18 | foreign_keys=[__table__.c.PackageID], 19 | ) 20 | 21 | Group = relationship( 22 | _Group, 23 | backref=backref("package_groups", lazy="dynamic", cascade="all, delete"), 24 | foreign_keys=[__table__.c.GroupID], 25 | ) 26 | 27 | def __init__(self, **kwargs): 28 | super().__init__(**kwargs) 29 | 30 | if not self.Package and not self.PackageID: 31 | raise IntegrityError( 32 | statement="Primary key PackageID cannot be null.", 33 | orig="PackageGroups.PackageID", 34 | params=("NULL"), 35 | ) 36 | 37 | if not self.Group and not self.GroupID: 38 | raise IntegrityError( 39 | statement="Primary key GroupID cannot be null.", 40 | orig="PackageGroups.GroupID", 41 | params=("NULL"), 42 | ) 43 | -------------------------------------------------------------------------------- /aurweb/models/package_keyword.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.exc import IntegrityError 2 | from sqlalchemy.orm import backref, relationship 3 | 4 | from aurweb import schema 5 | from aurweb.models.declarative import Base 6 | from aurweb.models.package_base import PackageBase as _PackageBase 7 | 8 | 9 | class PackageKeyword(Base): 10 | __table__ = schema.PackageKeywords 11 | __tablename__ = __table__.name 12 | __mapper_args__ = {"primary_key": [__table__.c.PackageBaseID, __table__.c.Keyword]} 13 | 14 | PackageBase = relationship( 15 | _PackageBase, 16 | backref=backref("keywords", lazy="dynamic", cascade="all, delete"), 17 | foreign_keys=[__table__.c.PackageBaseID], 18 | ) 19 | 20 | def __init__(self, **kwargs): 21 | super().__init__(**kwargs) 22 | 23 | if not self.PackageBase and not self.PackageBaseID: 24 | raise IntegrityError( 25 | statement="Primary key PackageBaseID cannot be null.", 26 | orig="PackageKeywords.PackageBaseID", 27 | params=("NULL"), 28 | ) 29 | -------------------------------------------------------------------------------- /aurweb/models/package_license.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.exc import IntegrityError 2 | from sqlalchemy.orm import backref, relationship 3 | 4 | from aurweb import schema 5 | from aurweb.models.declarative import Base 6 | from aurweb.models.license import License as _License 7 | from aurweb.models.package import Package as _Package 8 | 9 | 10 | class PackageLicense(Base): 11 | __table__ = schema.PackageLicenses 12 | __tablename__ = __table__.name 13 | __mapper_args__ = {"primary_key": [__table__.c.PackageID, __table__.c.LicenseID]} 14 | 15 | Package = relationship( 16 | _Package, 17 | backref=backref("package_licenses", lazy="dynamic", cascade="all, delete"), 18 | foreign_keys=[__table__.c.PackageID], 19 | ) 20 | 21 | License = relationship( 22 | _License, 23 | backref=backref("package_licenses", lazy="dynamic", cascade="all, delete"), 24 | foreign_keys=[__table__.c.LicenseID], 25 | ) 26 | 27 | def __init__(self, **kwargs): 28 | super().__init__(**kwargs) 29 | 30 | if not self.Package and not self.PackageID: 31 | raise IntegrityError( 32 | statement="Primary key PackageID cannot be null.", 33 | orig="PackageLicenses.PackageID", 34 | params=("NULL"), 35 | ) 36 | 37 | if not self.License and not self.LicenseID: 38 | raise IntegrityError( 39 | statement="Primary key LicenseID cannot be null.", 40 | orig="PackageLicenses.LicenseID", 41 | params=("NULL"), 42 | ) 43 | -------------------------------------------------------------------------------- /aurweb/models/package_notification.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.exc import IntegrityError 2 | from sqlalchemy.orm import backref, relationship 3 | 4 | from aurweb import schema 5 | from aurweb.models.declarative import Base 6 | from aurweb.models.package_base import PackageBase as _PackageBase 7 | from aurweb.models.user import User as _User 8 | 9 | 10 | class PackageNotification(Base): 11 | __table__ = schema.PackageNotifications 12 | __tablename__ = __table__.name 13 | __mapper_args__ = {"primary_key": [__table__.c.UserID, __table__.c.PackageBaseID]} 14 | 15 | User = relationship( 16 | _User, 17 | backref=backref("notifications", lazy="dynamic", cascade="all, delete"), 18 | foreign_keys=[__table__.c.UserID], 19 | ) 20 | 21 | PackageBase = relationship( 22 | _PackageBase, 23 | backref=backref("notifications", lazy="dynamic", cascade="all, delete"), 24 | foreign_keys=[__table__.c.PackageBaseID], 25 | ) 26 | 27 | def __init__(self, **kwargs): 28 | super().__init__(**kwargs) 29 | 30 | if not self.User and not self.UserID: 31 | raise IntegrityError( 32 | statement="Foreign key UserID cannot be null.", 33 | orig="PackageNotifications.UserID", 34 | params=("NULL"), 35 | ) 36 | 37 | if not self.PackageBase and not self.PackageBaseID: 38 | raise IntegrityError( 39 | statement="Foreign key PackageBaseID cannot be null.", 40 | orig="PackageNotifications.PackageBaseID", 41 | params=("NULL"), 42 | ) 43 | -------------------------------------------------------------------------------- /aurweb/models/package_source.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.exc import IntegrityError 2 | from sqlalchemy.orm import backref, relationship 3 | 4 | from aurweb import schema 5 | from aurweb.models.declarative import Base 6 | from aurweb.models.package import Package as _Package 7 | 8 | 9 | class PackageSource(Base): 10 | __table__ = schema.PackageSources 11 | __tablename__ = __table__.name 12 | __mapper_args__ = {"primary_key": [__table__.c.PackageID, __table__.c.Source]} 13 | 14 | Package = relationship( 15 | _Package, 16 | backref=backref("package_sources", lazy="dynamic", cascade="all, delete"), 17 | foreign_keys=[__table__.c.PackageID], 18 | ) 19 | 20 | def __init__(self, **kwargs): 21 | super().__init__(**kwargs) 22 | 23 | if not self.Package and not self.PackageID: 24 | raise IntegrityError( 25 | statement="Foreign key PackageID cannot be null.", 26 | orig="PackageSources.PackageID", 27 | params=("NULL"), 28 | ) 29 | 30 | if not self.Source: 31 | self.Source = "/dev/null" 32 | -------------------------------------------------------------------------------- /aurweb/models/package_vote.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.exc import IntegrityError 2 | from sqlalchemy.orm import backref, relationship 3 | 4 | from aurweb import schema 5 | from aurweb.models.declarative import Base 6 | from aurweb.models.package_base import PackageBase as _PackageBase 7 | from aurweb.models.user import User as _User 8 | 9 | 10 | class PackageVote(Base): 11 | __table__ = schema.PackageVotes 12 | __tablename__ = __table__.name 13 | __mapper_args__ = {"primary_key": [__table__.c.UsersID, __table__.c.PackageBaseID]} 14 | 15 | User = relationship( 16 | _User, 17 | backref=backref("package_votes", lazy="dynamic", cascade="all, delete"), 18 | foreign_keys=[__table__.c.UsersID], 19 | ) 20 | 21 | PackageBase = relationship( 22 | _PackageBase, 23 | backref=backref("package_votes", lazy="dynamic", cascade="all, delete"), 24 | foreign_keys=[__table__.c.PackageBaseID], 25 | ) 26 | 27 | def __init__(self, **kwargs): 28 | super().__init__(**kwargs) 29 | 30 | if not self.User and not self.UsersID: 31 | raise IntegrityError( 32 | statement="Foreign key UsersID cannot be null.", 33 | orig="PackageVotes.UsersID", 34 | params=("NULL"), 35 | ) 36 | 37 | if not self.PackageBase and not self.PackageBaseID: 38 | raise IntegrityError( 39 | statement="Foreign key PackageBaseID cannot be null.", 40 | orig="PackageVotes.PackageBaseID", 41 | params=("NULL"), 42 | ) 43 | 44 | if not self.VoteTS: 45 | raise IntegrityError( 46 | statement="Column VoteTS cannot be null.", 47 | orig="PackageVotes.VoteTS", 48 | params=("NULL"), 49 | ) 50 | -------------------------------------------------------------------------------- /aurweb/models/relation_type.py: -------------------------------------------------------------------------------- 1 | from aurweb import schema 2 | from aurweb.models.declarative import Base 3 | 4 | CONFLICTS = "conflicts" 5 | PROVIDES = "provides" 6 | REPLACES = "replaces" 7 | 8 | CONFLICTS_ID = 1 9 | PROVIDES_ID = 2 10 | REPLACES_ID = 3 11 | 12 | 13 | class RelationType(Base): 14 | __table__ = schema.RelationTypes 15 | __tablename__ = __table__.name 16 | __mapper_args__ = {"primary_key": [__table__.c.ID]} 17 | 18 | def __init__(self, Name: str = None): 19 | self.Name = Name 20 | -------------------------------------------------------------------------------- /aurweb/models/request_type.py: -------------------------------------------------------------------------------- 1 | from aurweb import schema 2 | from aurweb.models.declarative import Base 3 | 4 | DELETION = "deletion" 5 | ORPHAN = "orphan" 6 | MERGE = "merge" 7 | 8 | DELETION_ID = 1 9 | ORPHAN_ID = 2 10 | MERGE_ID = 3 11 | 12 | 13 | class RequestType(Base): 14 | __table__ = schema.RequestTypes 15 | __tablename__ = __table__.name 16 | __mapper_args__ = {"primary_key": [__table__.c.ID]} 17 | 18 | def name_display(self) -> str: 19 | """Return the Name column with its first char capitalized.""" 20 | return self.Name.title() 21 | -------------------------------------------------------------------------------- /aurweb/models/session.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.exc import IntegrityError 2 | from sqlalchemy.orm import backref, relationship 3 | 4 | from aurweb import db, schema 5 | from aurweb.models.declarative import Base 6 | from aurweb.models.user import User as _User 7 | 8 | 9 | class Session(Base): 10 | __table__ = schema.Sessions 11 | __tablename__ = __table__.name 12 | __mapper_args__ = {"primary_key": [__table__.c.UsersID]} 13 | 14 | User = relationship( 15 | _User, 16 | backref=backref("session", cascade="all, delete", uselist=False), 17 | foreign_keys=[__table__.c.UsersID], 18 | ) 19 | 20 | def __init__(self, **kwargs): 21 | super().__init__(**kwargs) 22 | 23 | # We'll try to either use UsersID or User.ID if we can. 24 | # If neither exist, an AttributeError is raised, in which case 25 | # we set the uid to 0, which triggers IntegrityError below. 26 | try: 27 | uid = self.UsersID or self.User.ID 28 | except AttributeError: 29 | uid = 0 30 | 31 | user_exists = db.query(_User).filter(_User.ID == uid).exists() 32 | if not db.query(user_exists).scalar(): 33 | raise IntegrityError( 34 | statement=( 35 | "Foreign key UsersID cannot be null and " 36 | "must be a valid user's ID." 37 | ), 38 | orig="Sessions.UsersID", 39 | params=("NULL"), 40 | ) 41 | 42 | 43 | def generate_unique_sid(): 44 | return db.make_random_value(Session, Session.SessionID, 32) 45 | -------------------------------------------------------------------------------- /aurweb/models/ssh_pub_key.py: -------------------------------------------------------------------------------- 1 | from subprocess import PIPE, Popen 2 | 3 | from sqlalchemy.orm import backref, relationship 4 | 5 | from aurweb import schema 6 | from aurweb.models.declarative import Base 7 | 8 | 9 | class SSHPubKey(Base): 10 | __table__ = schema.SSHPubKeys 11 | __tablename__ = __table__.name 12 | __mapper_args__ = {"primary_key": [__table__.c.Fingerprint]} 13 | 14 | User = relationship( 15 | "User", 16 | backref=backref("ssh_pub_keys", lazy="dynamic", cascade="all, delete"), 17 | foreign_keys=[__table__.c.UserID], 18 | ) 19 | 20 | def __init__(self, **kwargs): 21 | super().__init__(**kwargs) 22 | 23 | 24 | def get_fingerprint(pubkey: str) -> str: 25 | proc = Popen(["ssh-keygen", "-l", "-f", "-"], stdin=PIPE, stdout=PIPE, stderr=PIPE) 26 | out, _ = proc.communicate(pubkey.encode()) 27 | if proc.returncode: 28 | raise ValueError("The SSH public key is invalid.") 29 | return out.decode().split()[1].split(":", 1)[1] 30 | -------------------------------------------------------------------------------- /aurweb/models/term.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.exc import IntegrityError 2 | 3 | from aurweb import schema 4 | from aurweb.models.declarative import Base 5 | 6 | 7 | class Term(Base): 8 | __table__ = schema.Terms 9 | __tablename__ = __table__.name 10 | __mapper_args__ = {"primary_key": [__table__.c.ID]} 11 | 12 | def __init__(self, **kwargs): 13 | super().__init__(**kwargs) 14 | 15 | if not self.Description: 16 | raise IntegrityError( 17 | statement="Column Description cannot be null.", 18 | orig="Terms.Description", 19 | params=("NULL"), 20 | ) 21 | 22 | if not self.URL: 23 | raise IntegrityError( 24 | statement="Column URL cannot be null.", 25 | orig="Terms.URL", 26 | params=("NULL"), 27 | ) 28 | -------------------------------------------------------------------------------- /aurweb/models/vote.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.exc import IntegrityError 2 | from sqlalchemy.orm import backref, relationship 3 | 4 | from aurweb import schema 5 | from aurweb.models.declarative import Base 6 | from aurweb.models.user import User as _User 7 | from aurweb.models.voteinfo import VoteInfo as _VoteInfo 8 | 9 | 10 | class Vote(Base): 11 | __table__ = schema.Votes 12 | __tablename__ = __table__.name 13 | __mapper_args__ = {"primary_key": [__table__.c.VoteID, __table__.c.UserID]} 14 | 15 | VoteInfo = relationship( 16 | _VoteInfo, 17 | backref=backref("votes", lazy="dynamic"), 18 | foreign_keys=[__table__.c.VoteID], 19 | ) 20 | 21 | User = relationship( 22 | _User, 23 | backref=backref("votes", lazy="dynamic"), 24 | foreign_keys=[__table__.c.UserID], 25 | ) 26 | 27 | def __init__(self, **kwargs): 28 | super().__init__(**kwargs) 29 | 30 | if not self.VoteInfo and not self.VoteID: 31 | raise IntegrityError( 32 | statement="Foreign key VoteID cannot be null.", 33 | orig="Votes.VoteID", 34 | params=("NULL"), 35 | ) 36 | 37 | if not self.User and not self.UserID: 38 | raise IntegrityError( 39 | statement="Foreign key UserID cannot be null.", 40 | orig="Votes.UserID", 41 | params=("NULL"), 42 | ) 43 | -------------------------------------------------------------------------------- /aurweb/packages/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/aurweb/ae432337466c2e38a38eaa2fdb54f193cf6a958c/aurweb/packages/__init__.py -------------------------------------------------------------------------------- /aurweb/pkgbase/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/aurweb/ae432337466c2e38a38eaa2fdb54f193cf6a958c/aurweb/pkgbase/__init__.py -------------------------------------------------------------------------------- /aurweb/requests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/aurweb/ae432337466c2e38a38eaa2fdb54f193cf6a958c/aurweb/requests/__init__.py -------------------------------------------------------------------------------- /aurweb/requests/util.py: -------------------------------------------------------------------------------- 1 | from http import HTTPStatus 2 | 3 | from fastapi import HTTPException 4 | 5 | from aurweb import db 6 | from aurweb.models import PackageRequest 7 | 8 | 9 | def get_pkgreq_by_id(id: int) -> PackageRequest: 10 | pkgreq = db.query(PackageRequest).filter(PackageRequest.ID == id).first() 11 | if not pkgreq: 12 | raise HTTPException(status_code=HTTPStatus.NOT_FOUND) 13 | return db.refresh(pkgreq) 14 | -------------------------------------------------------------------------------- /aurweb/routers/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | API routers for FastAPI. 3 | 4 | See https://fastapi.tiangolo.com/tutorial/bigger-applications/ 5 | """ 6 | 7 | from . import ( 8 | accounts, 9 | auth, 10 | html, 11 | package_maintainer, 12 | packages, 13 | pkgbase, 14 | requests, 15 | rpc, 16 | rss, 17 | sso, 18 | ) 19 | 20 | """ 21 | aurweb application routes. This constant can be any iterable 22 | and each element must have a .router attribute which points 23 | to a fastapi.APIRouter. 24 | """ 25 | APP_ROUTES = [ 26 | accounts, 27 | auth, 28 | html, 29 | packages, 30 | pkgbase, 31 | requests, 32 | package_maintainer, 33 | rss, 34 | rpc, 35 | sso, 36 | ] 37 | -------------------------------------------------------------------------------- /aurweb/scripts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/aurweb/ae432337466c2e38a38eaa2fdb54f193cf6a958c/aurweb/scripts/__init__.py -------------------------------------------------------------------------------- /aurweb/scripts/pkgmaint.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from sqlalchemy import and_ 4 | 5 | from aurweb import db, time 6 | from aurweb.models import PackageBase 7 | 8 | 9 | def _main(): 10 | # One day behind. 11 | limit_to = time.utcnow() - 86400 12 | 13 | query = db.query(PackageBase).filter( 14 | and_(PackageBase.SubmittedTS < limit_to, PackageBase.PackagerUID.is_(None)) 15 | ) 16 | db.delete_all(query) 17 | 18 | 19 | def main(): 20 | # Previously used to clean up "reserved" packages which never got pushed. 21 | # Let's deactivate this for now since "setup-repo" is gone and we see 22 | # other issue where deletion of a user account might cause unintended 23 | # removal of a package (where PackagerUID account was deleted) 24 | return 25 | 26 | db.get_engine() 27 | with db.begin(): 28 | _main() 29 | 30 | 31 | if __name__ == "__main__": 32 | main() 33 | -------------------------------------------------------------------------------- /aurweb/scripts/usermaint.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from sqlalchemy import update 4 | 5 | from aurweb import db, time 6 | from aurweb.models import User 7 | 8 | 9 | def _main(): 10 | limit_to = time.utcnow() - 86400 * 7 11 | 12 | update_ = ( 13 | update(User).where(User.LastLogin < limit_to).values(LastLoginIPAddress=None) 14 | ) 15 | db.get_session().execute(update_) 16 | 17 | update_ = ( 18 | update(User) 19 | .where(User.LastSSHLogin < limit_to) 20 | .values(LastSSHLoginIPAddress=None) 21 | ) 22 | db.get_session().execute(update_) 23 | 24 | 25 | def main(): 26 | db.get_engine() 27 | with db.begin(): 28 | _main() 29 | 30 | 31 | if __name__ == "__main__": 32 | main() 33 | -------------------------------------------------------------------------------- /aurweb/scripts/votereminder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from sqlalchemy import and_ 4 | 5 | import aurweb.config 6 | from aurweb import db, time 7 | from aurweb.models import VoteInfo 8 | from aurweb.scripts import notify 9 | 10 | notify_cmd = aurweb.config.get("notifications", "notify-cmd") 11 | 12 | 13 | def main(): 14 | db.get_engine() 15 | 16 | now = time.utcnow() 17 | 18 | start = aurweb.config.getint("votereminder", "range_start") 19 | filter_from = now + start 20 | 21 | end = aurweb.config.getint("votereminder", "range_end") 22 | filter_to = now + end 23 | 24 | query = db.query(VoteInfo.ID).filter( 25 | and_(VoteInfo.End >= filter_from, VoteInfo.End <= filter_to) 26 | ) 27 | for voteinfo in query: 28 | notif = notify.VoteReminderNotification(voteinfo.ID) 29 | notif.send() 30 | 31 | 32 | if __name__ == "__main__": 33 | main() 34 | -------------------------------------------------------------------------------- /aurweb/testing/filelock.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import os 3 | from typing import Callable 4 | 5 | from posix_ipc import O_CREAT, Semaphore 6 | 7 | from aurweb import aur_logging 8 | 9 | logger = aur_logging.get_logger(__name__) 10 | 11 | 12 | def default_on_create(path): 13 | logger.info(f"Filelock at {path} acquired.") 14 | 15 | 16 | class FileLock: 17 | def __init__(self, tmpdir, name: str): 18 | self.root = tmpdir 19 | self.path = str(self.root / name) 20 | self._file = str(self.root / (f"{name}.1")) 21 | 22 | def lock(self, on_create: Callable = default_on_create): 23 | hash = hashlib.sha1(self.path.encode()).hexdigest() 24 | with Semaphore(f"/{hash}-lock", flags=O_CREAT, initial_value=1): 25 | retval = os.path.exists(self._file) 26 | if not retval: 27 | with open(self._file, "w") as f: 28 | f.write("1") 29 | on_create(self.path) 30 | 31 | return retval 32 | -------------------------------------------------------------------------------- /aurweb/testing/html.py: -------------------------------------------------------------------------------- 1 | from io import StringIO 2 | 3 | from lxml import etree 4 | 5 | parser = etree.HTMLParser() 6 | 7 | 8 | def parse_root(html: str) -> etree.Element: 9 | """Parse an lxml.etree.ElementTree root from html content. 10 | 11 | :param html: HTML markup 12 | :return: etree.Element 13 | """ 14 | return etree.parse(StringIO(html), parser) 15 | 16 | 17 | def get_errors(content: str) -> list[etree._Element]: 18 | root = parse_root(content) 19 | return root.xpath('//ul[@class="errorlist"]/li') 20 | 21 | 22 | def get_successes(content: str) -> list[etree._Element]: 23 | root = parse_root(content) 24 | return root.xpath('//ul[@class="success"]/li') 25 | -------------------------------------------------------------------------------- /aurweb/testing/prometheus.py: -------------------------------------------------------------------------------- 1 | from aurweb import prometheus 2 | 3 | 4 | def clear_metrics(): 5 | prometheus.PACKAGES.clear() 6 | prometheus.REQUESTS.clear() 7 | prometheus.SEARCH_REQUESTS.clear() 8 | prometheus.USERS.clear() 9 | -------------------------------------------------------------------------------- /aurweb/testing/requests.py: -------------------------------------------------------------------------------- 1 | import aurweb.config 2 | 3 | 4 | class User: 5 | """A fake User model.""" 6 | 7 | # Fake columns. 8 | LangPreference = aurweb.config.get("options", "default_lang") 9 | Timezone = aurweb.config.get("options", "default_timezone") 10 | 11 | # A fake authenticated flag. 12 | authenticated = False 13 | 14 | def is_authenticated(self): 15 | return self.authenticated 16 | 17 | 18 | class Client: 19 | """A fake FastAPI Request.client object.""" 20 | 21 | # A fake host. 22 | host = "127.0.0.1" 23 | 24 | 25 | class URL: 26 | path: str 27 | 28 | def __init__(self, path: str = "/"): 29 | self.path = path 30 | 31 | 32 | class Request: 33 | """A fake Request object which mimics a FastAPI Request for tests.""" 34 | 35 | client = Client() 36 | url = URL() 37 | 38 | def __init__( 39 | self, 40 | user: User = User(), 41 | authenticated: bool = False, 42 | method: str = "GET", 43 | headers: dict[str, str] = dict(), 44 | cookies: dict[str, str] = dict(), 45 | url: str = "/", 46 | query_params: dict[str, str] = dict(), 47 | ) -> "Request": 48 | self.user = user 49 | self.user.authenticated = authenticated 50 | 51 | self.method = method.upper() 52 | self.headers = headers 53 | self.cookies = cookies 54 | self.url = URL(path=url) 55 | self.query_params = query_params 56 | -------------------------------------------------------------------------------- /aurweb/testing/smtp.py: -------------------------------------------------------------------------------- 1 | """Fake SMTP clients that can be used for testing.""" 2 | 3 | 4 | class FakeSMTP: 5 | """A fake version of smtplib.SMTP used for testing.""" 6 | 7 | starttls_enabled = False 8 | use_ssl = False 9 | 10 | def __init__(self): 11 | self.emails = [] 12 | self.count = 0 13 | self.ehlo_count = 0 14 | self.quit_count = 0 15 | self.set_debuglevel_count = 0 16 | self.user = None 17 | self.passwd = None 18 | 19 | def ehlo(self) -> None: 20 | self.ehlo_count += 1 21 | 22 | def starttls(self) -> None: 23 | self.starttls_enabled = True 24 | 25 | def set_debuglevel(self, level: int = 0) -> None: 26 | self.set_debuglevel_count += 1 27 | 28 | def login(self, user: str, passwd: str) -> None: 29 | self.user = user 30 | self.passwd = passwd 31 | 32 | def sendmail(self, sender: str, to: str, msg: bytes) -> None: 33 | self.emails.append((sender, to, msg.decode())) 34 | self.count += 1 35 | 36 | def quit(self) -> None: 37 | self.quit_count += 1 38 | 39 | def __call__(self, *args, **kwargs) -> "FakeSMTP": 40 | return self 41 | 42 | 43 | class FakeSMTP_SSL(FakeSMTP): 44 | """A fake version of smtplib.SMTP_SSL used for testing.""" 45 | 46 | use_ssl = True 47 | -------------------------------------------------------------------------------- /aurweb/users/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/aurweb/ae432337466c2e38a38eaa2fdb54f193cf6a958c/aurweb/users/__init__.py -------------------------------------------------------------------------------- /aurweb/users/util.py: -------------------------------------------------------------------------------- 1 | from http import HTTPStatus 2 | 3 | from fastapi import HTTPException 4 | 5 | from aurweb import db 6 | from aurweb.models import User 7 | 8 | 9 | def get_user_by_name(username: str) -> User: 10 | """ 11 | Query a user by its username. 12 | 13 | :param username: User.Username 14 | :return: User instance 15 | """ 16 | user = db.query(User).filter(User.Username == username).first() 17 | if not user: 18 | raise HTTPException(status_code=int(HTTPStatus.NOT_FOUND)) 19 | return db.refresh(user) 20 | -------------------------------------------------------------------------------- /cache/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/aurweb/ae432337466c2e38a38eaa2fdb54f193cf6a958c/cache/.gitkeep -------------------------------------------------------------------------------- /ci/tf/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | backend "http" { 3 | } 4 | } 5 | 6 | provider "hcloud" { 7 | token = var.hcloud_token 8 | } 9 | 10 | provider "dns" { 11 | update { 12 | server = var.dns_server 13 | key_name = var.dns_tsig_key 14 | key_algorithm = var.dns_tsig_algorithm 15 | key_secret = var.dns_tsig_secret 16 | } 17 | } 18 | 19 | resource "tls_private_key" "this" { 20 | algorithm = "ED25519" 21 | } 22 | 23 | resource "hcloud_ssh_key" "this" { 24 | name = var.name 25 | public_key = tls_private_key.this.public_key_openssh 26 | } 27 | 28 | data "hcloud_image" "this" { 29 | with_selector = "custom_image=archlinux" 30 | most_recent = true 31 | with_status = ["available"] 32 | } 33 | 34 | resource "hcloud_server" "this" { 35 | name = var.name 36 | image = data.hcloud_image.this.id 37 | server_type = var.server_type 38 | datacenter = var.datacenter 39 | ssh_keys = [hcloud_ssh_key.this.name] 40 | 41 | public_net { 42 | ipv4_enabled = true 43 | ipv6_enabled = true 44 | } 45 | } 46 | 47 | resource "hcloud_rdns" "this" { 48 | for_each = { ipv4 : hcloud_server.this.ipv4_address, ipv6 : hcloud_server.this.ipv6_address } 49 | 50 | server_id = hcloud_server.this.id 51 | ip_address = each.value 52 | dns_ptr = "${var.name}.${var.dns_zone}" 53 | } 54 | 55 | resource "dns_a_record_set" "this" { 56 | zone = "${var.dns_zone}." 57 | name = var.name 58 | addresses = [hcloud_server.this.ipv4_address] 59 | ttl = 300 60 | } 61 | 62 | resource "dns_aaaa_record_set" "this" { 63 | zone = "${var.dns_zone}." 64 | name = var.name 65 | addresses = [hcloud_server.this.ipv6_address] 66 | ttl = 300 67 | } 68 | -------------------------------------------------------------------------------- /ci/tf/terraform.tfvars: -------------------------------------------------------------------------------- 1 | server_type = "cpx11" 2 | datacenter = "fsn1-dc14" 3 | dns_server = "redirect.archlinux.org" 4 | dns_zone = "sandbox.archlinux.page" 5 | -------------------------------------------------------------------------------- /ci/tf/variables.tf: -------------------------------------------------------------------------------- 1 | variable "hcloud_token" { 2 | type = string 3 | sensitive = true 4 | } 5 | 6 | variable "dns_server" { 7 | type = string 8 | } 9 | 10 | variable "dns_tsig_key" { 11 | type = string 12 | } 13 | 14 | variable "dns_tsig_algorithm" { 15 | type = string 16 | } 17 | 18 | variable "dns_tsig_secret" { 19 | type = string 20 | } 21 | 22 | variable "dns_zone" { 23 | type = string 24 | } 25 | 26 | variable "name" { 27 | type = string 28 | } 29 | 30 | variable "server_type" { 31 | type = string 32 | } 33 | 34 | variable "datacenter" { 35 | type = string 36 | } 37 | -------------------------------------------------------------------------------- /ci/tf/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | tls = { 4 | source = "hashicorp/tls" 5 | } 6 | hcloud = { 7 | source = "hetznercloud/hcloud" 8 | } 9 | dns = { 10 | source = "hashicorp/dns" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /conf/cgitrc.proto: -------------------------------------------------------------------------------- 1 | virtual-root=/cgit/ 2 | clone-prefix=https://aur.archlinux.org 3 | noheader=0 4 | favicon=/images/favicon.ico 5 | logo= 6 | css=/css/cgit.css 7 | snapshots=tar.gz 8 | readme=:README.md 9 | readme=:README 10 | enable-index-owner=0 11 | enable-index-links=1 12 | 13 | cache-root=/var/cache/cgit 14 | cache-size=500000 15 | cache-dynamic-ttl=15 16 | cache-repo-ttl=15 17 | cache-root-ttl=60 18 | cache-scanrc-ttl=120 19 | cache-static-ttl=60 20 | 21 | root-title=AUR Package Repositories 22 | root-desc=Web interface to the AUR Package Repositories 23 | header=/srv/http/aurweb/static/html/cgit/header.html 24 | footer=/srv/http/aurweb/static/html/cgit/footer.html 25 | max-repodesc-length=50 26 | max-blob-size=2048 27 | max-stats=year 28 | enable-http-clone=1 29 | 30 | repo.url=aur.git 31 | repo.path=/srv/http/aurweb/aur.git 32 | repo.desc=AUR Package Repositories 33 | -------------------------------------------------------------------------------- /conf/fcgiwrap.service.proto: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Simple CGI Server 3 | After=nss-user-lookup.target 4 | 5 | [Service] 6 | ExecStart=/usr/sbin/fcgiwrap 7 | User=aur 8 | Group=aur 9 | 10 | [Install] 11 | Also=fcgiwrap.socket 12 | -------------------------------------------------------------------------------- /conf/fcgiwrap.socket.proto: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=fcgiwrap Socket 3 | 4 | [Socket] 5 | ListenStream=/run/fcgiwrap.sock 6 | SocketUser=http 7 | SocketGroup=http 8 | SocketMode=0700 9 | 10 | [Install] 11 | WantedBy=sockets.target 12 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | all: rpc.html 2 | 3 | clean: 4 | rm -rf *.html 5 | 6 | %.html: %.txt 7 | asciidoc $< 8 | 9 | .PHONY: all clean 10 | -------------------------------------------------------------------------------- /doc/repos/pkgbases-repo.md: -------------------------------------------------------------------------------- 1 | # Repository: pkgbases-repo 2 | 3 | ## Overview 4 | 5 | - `pkgbase.json` contains a list of package base names 6 | 7 | ## File Layout 8 | 9 | ### pkgbase.json: 10 | 11 | [ 12 | "pkgbase1", 13 | "pkgbase2", 14 | ... 15 | ] 16 | -------------------------------------------------------------------------------- /doc/repos/pkgnames-repo.md: -------------------------------------------------------------------------------- 1 | # Repository: pkgnames-repo 2 | 3 | ## Overview 4 | 5 | - `pkgname.json` contains a list of package names 6 | 7 | ## File Layout 8 | 9 | ### pkgname.json: 10 | 11 | [ 12 | "pkgname1", 13 | "pkgname2", 14 | ... 15 | ] 16 | -------------------------------------------------------------------------------- /doc/repos/users-repo.md: -------------------------------------------------------------------------------- 1 | # Repository: users-repo 2 | 3 | ## Overview 4 | 5 | - `users.json` contains a list of usernames 6 | 7 | ## File Layout 8 | 9 | ### users.json: 10 | 11 | [ 12 | "user1", 13 | "user2", 14 | ... 15 | ] 16 | -------------------------------------------------------------------------------- /doc/specs/metadata.md: -------------------------------------------------------------------------------- 1 | # Git Archive Spec: metadata 2 | 3 | ## Configuration 4 | 5 | - `[git-archive]` 6 | - `metadata-repo` 7 | - Path to package metadata git repository location 8 | 9 | ## Repositories 10 | 11 | For documentation on each one of these repositories, follow their link, 12 | which brings you to a topical markdown for that repository. 13 | 14 | - [metadata-repo](doc/repos/metadata-repo.md) 15 | -------------------------------------------------------------------------------- /doc/specs/pkgbases.md: -------------------------------------------------------------------------------- 1 | # Git Archive Spec: pkgbases 2 | 3 | ## Configuration 4 | 5 | - `[git-archive]` 6 | - `pkgbases-repo` 7 | - Path to pkgbases git repository location 8 | 9 | ## Repositories 10 | 11 | For documentation on each one of these repositories, follow their link, 12 | which brings you to a topical markdown for that repository. 13 | 14 | - [pkgbases-repo](doc/repos/pkgbases-repo.md) 15 | -------------------------------------------------------------------------------- /doc/specs/pkgnames.md: -------------------------------------------------------------------------------- 1 | # Git Archive Spec: pkgnames 2 | 3 | ## Configuration 4 | 5 | - `[git-archive]` 6 | - `pkgnames-repo` 7 | - Path to pkgnames git repository location 8 | 9 | ## Repositories 10 | 11 | For documentation on each one of these repositories, follow their link, 12 | which brings you to a topical markdown for that repository. 13 | 14 | - [pkgnames-repo](doc/repos/pkgnames-repo.md) 15 | -------------------------------------------------------------------------------- /doc/specs/popularity.md: -------------------------------------------------------------------------------- 1 | # Git Archive Spec: popularity 2 | 3 | ## Configuration 4 | 5 | - `[git-archive]` 6 | - `popularity-repo` 7 | - Path to popularity git repository location 8 | 9 | ## Repositories 10 | 11 | For documentation on each one of these repositories, follow their link, 12 | which brings you to a topical markdown for that repository. 13 | 14 | - [popularity-repo](doc/repos/popularity-repo.md) 15 | -------------------------------------------------------------------------------- /doc/specs/users.md: -------------------------------------------------------------------------------- 1 | # Git Archive Spec: users 2 | 3 | ## Configuration 4 | 5 | - `[git-archive]` 6 | - `users-repo` 7 | - Path to users git repository location 8 | 9 | ## Repositories 10 | 11 | For documentation on each one of these repositories, follow their link, 12 | which brings you to a topical markdown for that repository. 13 | 14 | - [users-repo](doc/repos/users-repo.md) 15 | -------------------------------------------------------------------------------- /doc/sso.txt: -------------------------------------------------------------------------------- 1 | Single Sign-On (SSO) 2 | ==================== 3 | 4 | This guide will walk you through setting up Keycloak for use with aurweb. For 5 | extensive documentation, see . 6 | 7 | Installing Keycloak 8 | ------------------- 9 | 10 | Keycloak is in the official Arch repositories: 11 | 12 | # pacman -S keycloak 13 | 14 | The default port is 8080, which conflicts with aurweb’s default port. You need 15 | to edit `/etc/keycloak/standalone.xml`, looking for this line: 16 | 17 | 18 | 19 | The default developer configuration assumes it is set to 8083. Alternatively, 20 | you may customize [options] aur_location and [sso] openid_configuration in 21 | `conf/config`. 22 | 23 | You may then start `keycloak.service` through systemd. 24 | 25 | See also ArchWiki . 26 | 27 | Configuring a realm 28 | ------------------- 29 | 30 | Go to and log in as administrator. Then, hover the 31 | text right below the Keycloak logo at the top left, by default *Master*. Click 32 | *Add realm* and name it *aurweb*. 33 | 34 | Open the *Clients* tab, and create a new *openid-connect* client. Call it 35 | *aurweb*, and set the root URL to (your aur_location). 36 | 37 | Create a user from the *Users* tab and try logging in from 38 | . 39 | -------------------------------------------------------------------------------- /docker-compose.aur-dev.yml: -------------------------------------------------------------------------------- 1 | --- 2 | services: 3 | ca: 4 | volumes: 5 | - data:/data 6 | - step:/root/.step 7 | 8 | redis: 9 | restart: always 10 | 11 | mariadb: 12 | restart: always 13 | 14 | git: 15 | restart: always 16 | environment: 17 | - AUR_CONFIG=/aurweb/conf/config 18 | # SSH_CMDLINE should be updated to production's ssh cmdline. 19 | - SSH_CMDLINE=${SSH_CMDLINE:-ssh ssh://aur@localhost:2222} 20 | volumes: 21 | - ${GIT_DATA_DIR}:/aurweb/aur.git 22 | - data:/aurweb/data 23 | 24 | smartgit: 25 | restart: always 26 | volumes: 27 | - ${GIT_DATA_DIR}:/aurweb/aur.git 28 | - data:/data 29 | - smartgit_run:/var/run/smartgit 30 | 31 | cgit-fastapi: 32 | restart: always 33 | volumes: 34 | - ${GIT_DATA_DIR}:/aurweb/aur.git 35 | 36 | cron: 37 | volumes: 38 | # Exclude ./aurweb:/aurweb in production. 39 | - mariadb_run:/var/run/mysqld 40 | - archives:/var/lib/aurweb/archives 41 | 42 | fastapi: 43 | restart: always 44 | environment: 45 | - COMMIT_HASH=$COMMIT_HASH 46 | - FASTAPI_BACKEND="gunicorn" 47 | - FASTAPI_WORKERS=${FASTAPI_WORKERS} 48 | - AURWEB_FASTAPI_PREFIX=${AURWEB_FASTAPI_PREFIX} 49 | - AURWEB_SSHD_PREFIX=${AURWEB_SSHD_PREFIX} 50 | - PROMETHEUS_MULTIPROC_DIR=/tmp_prometheus 51 | volumes: 52 | - data:/data 53 | 54 | nginx: 55 | restart: always 56 | volumes: 57 | - data:/data 58 | - archives:/var/lib/aurweb/archives 59 | - smartgit_run:/var/run/smartgit 60 | 61 | volumes: 62 | mariadb_run: {} # Share /var/run/mysqld 63 | mariadb_data: {} # Share /var/lib/mysql 64 | git_data: {} # Share aurweb/aur.git 65 | smartgit_run: {} 66 | data: {} 67 | logs: {} 68 | -------------------------------------------------------------------------------- /docker-compose.override.yml: -------------------------------------------------------------------------------- 1 | --- 2 | services: 3 | ca: 4 | volumes: 5 | - ./data:/data 6 | - step:/root/.step 7 | 8 | git: 9 | volumes: 10 | - git_data:/aurweb/aur.git 11 | - ./data:/aurweb/data 12 | 13 | smartgit: 14 | volumes: 15 | - git_data:/aurweb/aur.git 16 | - ./data:/data 17 | - smartgit_run:/var/run/smartgit 18 | 19 | fastapi: 20 | volumes: 21 | - ./data:/data 22 | - ./aurweb:/aurweb/aurweb 23 | - ./migrations:/aurweb/migrations 24 | - ./test:/aurweb/test 25 | - ./templates:/aurweb/templates 26 | - ./schema:/aurweb/schema 27 | 28 | nginx: 29 | volumes: 30 | - ./data:/data 31 | - archives:/var/lib/aurweb/archives 32 | - smartgit_run:/var/run/smartgit 33 | 34 | mariadb: 35 | volumes: 36 | - ./schema:/aurweb/schema 37 | -------------------------------------------------------------------------------- /docker/README.md: -------------------------------------------------------------------------------- 1 | # Aurweb and Docker 2 | 3 | The `INSTALL` document details a manual Aurweb setup, but Docker images are also 4 | provided here to avoid the complications of database configuration (and so 5 | forth). 6 | 7 | ### Setup 8 | 9 | Naturally, both `docker` and `docker-compose` must be installed, and your Docker 10 | service must be started: 11 | 12 | ```sh 13 | systemctl start docker.service 14 | ``` 15 | 16 | The main image - `aurweb` - must be built manually: 17 | 18 | ```sh 19 | docker compose build 20 | ``` 21 | 22 | ### Starting and Stopping the Services 23 | 24 | With the above steps complete, you can bring up an initial cluster: 25 | 26 | ```sh 27 | docker compose up 28 | ``` 29 | 30 | Subsequent runs will be done with `start` instead of `up`. The cluster can be 31 | stopped with `docker compose stop`. 32 | 33 | ### Testing 34 | 35 | With a running cluster, execute the following in a new terminal: 36 | 37 | ```sh 38 | docker compose run test 39 | ``` 40 | 41 | ### Generating Dummy Data 42 | 43 | Before you can make meaningful queries to the cluster, it needs some data. 44 | Luckily such data can be generated. 45 | 46 | ```sh 47 | docker compose exec fastapi /bin/bash 48 | pacman -S words fortune-mod 49 | ./schema/gendummydata.py dummy.sql 50 | mariadb aurweb < dummy.sql 51 | ``` 52 | 53 | The generation script may prompt you to install other Arch packages before it 54 | can proceed. 55 | 56 | ### Querying the RPC 57 | 58 | The Fast (Python) API runs on Port 8444. You can query one like so: 59 | 60 | ```sh 61 | curl -k "https://localhost:8444/rpc/?v=5&type=search&arg=python" 62 | ``` 63 | 64 | `-k` bypasses local certificate issues that `curl` will otherwise complain about. 65 | -------------------------------------------------------------------------------- /docker/cgit-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eou pipefail 3 | 4 | mkdir -p /var/data/cgit 5 | 6 | cp -vf conf/cgitrc.proto /etc/cgitrc 7 | sed -ri "s|clone-prefix=.*|clone-prefix=${CGIT_CLONE_PREFIX}|" /etc/cgitrc 8 | sed -ri 's|header=.*|header=/aurweb/static/html/cgit/header.html|' /etc/cgitrc 9 | sed -ri 's|footer=.*|footer=/aurweb/static/html/cgit/footer.html|' /etc/cgitrc 10 | sed -ri 's|repo\.path=.*|repo.path=/aurweb/aur.git|' /etc/cgitrc 11 | sed -ri "s|^(css)=.*$|\1=${CGIT_CSS}|" /etc/cgitrc 12 | 13 | exec "$@" 14 | -------------------------------------------------------------------------------- /docker/config/aurweb-cron: -------------------------------------------------------------------------------- 1 | AUR_CONFIG='/aurweb/conf/config' 2 | */5 * * * * bash -c 'aurweb-mkpkglists --extended' 3 | */2 * * * * bash -c 'aurweb-aurblup' 4 | */2 * * * * bash -c 'aurweb-pkgmaint' 5 | */2 * * * * bash -c 'aurweb-usermaint' 6 | */2 * * * * bash -c 'aurweb-popupdate' 7 | */12 * * * * bash -c 'aurweb-votereminder' 8 | -------------------------------------------------------------------------------- /docker/config/grafana/datasources/datasource.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: 1 3 | 4 | deleteDatasources: 5 | - name: Prometheus 6 | - name: Tempo 7 | 8 | datasources: 9 | - name: Prometheus 10 | type: prometheus 11 | uid: prometheus 12 | access: proxy 13 | url: http://prometheus:9090 14 | orgId: 1 15 | editable: false 16 | jsonData: 17 | timeInterval: 1m 18 | - name: Tempo 19 | type: tempo 20 | uid: tempo 21 | access: proxy 22 | url: http://tempo:3200 23 | orgId: 1 24 | editable: false 25 | jsonData: 26 | tracesToMetrics: 27 | datasourceUid: 'prometheus' 28 | spanStartTimeShift: '1h' 29 | spanEndTimeShift: '-1h' 30 | serviceMap: 31 | datasourceUid: 'prometheus' 32 | nodeGraph: 33 | enabled: true 34 | search: 35 | hide: false 36 | traceQuery: 37 | timeShiftEnabled: true 38 | spanStartTimeShift: '1h' 39 | spanEndTimeShift: '-1h' 40 | spanBar: 41 | type: 'Tag' 42 | tag: 'http.path' 43 | -------------------------------------------------------------------------------- /docker/config/prometheus.yml: -------------------------------------------------------------------------------- 1 | --- 2 | global: 3 | scrape_interval: 60s 4 | 5 | scrape_configs: 6 | - job_name: tempo 7 | static_configs: 8 | - targets: ['tempo:3200'] 9 | labels: 10 | instance: tempo 11 | - job_name: aurweb 12 | static_configs: 13 | - targets: ['fastapi:8000'] 14 | labels: 15 | instance: aurweb 16 | -------------------------------------------------------------------------------- /docker/config/tempo.yml: -------------------------------------------------------------------------------- 1 | --- 2 | stream_over_http_enabled: true 3 | server: 4 | http_listen_address: tempo 5 | http_listen_port: 3200 6 | log_level: info 7 | 8 | query_frontend: 9 | search: 10 | duration_slo: 5s 11 | throughput_bytes_slo: 1.073741824e+09 12 | trace_by_id: 13 | duration_slo: 5s 14 | 15 | distributor: 16 | receivers: 17 | otlp: 18 | protocols: 19 | http: 20 | endpoint: tempo:4318 21 | log_received_spans: 22 | enabled: false 23 | metric_received_spans: 24 | enabled: false 25 | 26 | ingester: 27 | max_block_duration: 5m 28 | 29 | compactor: 30 | compaction: 31 | block_retention: 1h 32 | 33 | metrics_generator: 34 | registry: 35 | external_labels: 36 | source: tempo 37 | storage: 38 | path: /tmp/tempo/generator/wal 39 | remote_write: 40 | - url: http://prometheus:9090/api/v1/write 41 | send_exemplars: true 42 | traces_storage: 43 | path: /tmp/tempo/generator/traces 44 | 45 | storage: 46 | trace: 47 | backend: local 48 | wal: 49 | path: /tmp/tempo/wal 50 | local: 51 | path: /tmp/tempo/blocks 52 | 53 | overrides: 54 | metrics_generator_processors: [service-graphs, span-metrics, local-blocks] 55 | -------------------------------------------------------------------------------- /docker/cron-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eou pipefail 3 | 4 | # Setup the DB. 5 | NO_INITDB=1 /docker/mariadb-init-entrypoint.sh 6 | 7 | # Create aurblup's directory. 8 | AURBLUP_DIR="/aurweb/aurblup/" 9 | mkdir -p $AURBLUP_DIR 10 | 11 | # Setup aurblup config for Docker. 12 | AURBLUP_DBS='core extra multilib core-testing extra-testing multilib-testing' 13 | AURBLUP_SERVER='https://mirrors.kernel.org/archlinux/%s/os/x86_64' 14 | aurweb-config set aurblup db-path "$AURBLUP_DIR" 15 | aurweb-config set aurblup sync-dbs "$AURBLUP_DBS" 16 | aurweb-config set aurblup server "$AURBLUP_SERVER" 17 | 18 | # Setup mkpkglists config for Docker. 19 | ARCHIVE_DIR='/var/lib/aurweb/archives' 20 | aurweb-config set mkpkglists archivedir "$ARCHIVE_DIR" 21 | aurweb-config set mkpkglists packagesfile "$ARCHIVE_DIR/packages.gz" 22 | aurweb-config set mkpkglists packagesmetafile \ 23 | "$ARCHIVE_DIR/packages-meta-v1.json.gz" 24 | aurweb-config set mkpkglists packagesmetaextfile \ 25 | "$ARCHIVE_DIR/packages-meta-ext-v1.json.gz" 26 | aurweb-config set mkpkglists pkgbasefile "$ARCHIVE_DIR/pkgbase.gz" 27 | aurweb-config set mkpkglists userfile "$ARCHIVE_DIR/users.gz" 28 | 29 | # Install the cron configuration. 30 | cp /docker/config/aurweb-cron /etc/cron.d/aurweb-cron 31 | chmod 0644 /etc/cron.d/aurweb-cron 32 | crontab /etc/cron.d/aurweb-cron 33 | 34 | # Remove leftover lockfile if restarted while aurblup syncs the database 35 | rm -f /aurweb/aurblup/db.lck 36 | 37 | exec "$@" 38 | -------------------------------------------------------------------------------- /docker/fastapi-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eou pipefail 3 | 4 | # Setup database. 5 | NO_INITDB=1 /docker/mariadb-init-entrypoint.sh 6 | 7 | # Setup some other options. 8 | aurweb-config set options cache 'redis' 9 | aurweb-config set options redis_address 'redis://redis' 10 | aurweb-config set options aur_location "$AURWEB_FASTAPI_PREFIX" 11 | aurweb-config set options git_clone_uri_anon "${AURWEB_FASTAPI_PREFIX}/%s.git" 12 | aurweb-config set options git_clone_uri_priv "${AURWEB_SSHD_PREFIX}/%s.git" 13 | 14 | if [ ! -z ${COMMIT_HASH+x} ]; then 15 | aurweb-config set devel commit_hash "$COMMIT_HASH" 16 | fi 17 | 18 | # Setup prometheus directory. 19 | rm -rf $PROMETHEUS_MULTIPROC_DIR 20 | mkdir -p $PROMETHEUS_MULTIPROC_DIR 21 | 22 | exec "$@" 23 | -------------------------------------------------------------------------------- /docker/health/ca.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | exec curl -qkiI 'https://localhost:8443/' 3 | -------------------------------------------------------------------------------- /docker/health/cgit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | exec printf "" >>/dev/tcp/127.0.0.1/${1} 3 | -------------------------------------------------------------------------------- /docker/health/fastapi.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | exec pgrep "$1" 3 | -------------------------------------------------------------------------------- /docker/health/mariadb.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | exec mariadb-admin ping --silent 3 | -------------------------------------------------------------------------------- /docker/health/nginx.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | exec curl -k -q https://localhost:8444 3 | -------------------------------------------------------------------------------- /docker/health/prometheus.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | exec wget -q http://prometheus:9090/status -O /dev/null 3 | -------------------------------------------------------------------------------- /docker/health/redis.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | exec pgrep redis-server 3 | -------------------------------------------------------------------------------- /docker/health/smartgit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | exec pgrep uwsgi 3 | -------------------------------------------------------------------------------- /docker/health/sshd.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Opt to just pgrep sshd instead of connecting here. This health 3 | # script is used on a regular interval and it ends up spamming 4 | # the git service's logs with accesses. 5 | exec pgrep sshd 6 | -------------------------------------------------------------------------------- /docker/health/tempo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | exec wget -q http://tempo:3200/status -O /dev/null 3 | -------------------------------------------------------------------------------- /docker/hypercorn.env: -------------------------------------------------------------------------------- 1 | FASTAPI_BACKEND="hypercorn" 2 | -------------------------------------------------------------------------------- /docker/localhost.ext: -------------------------------------------------------------------------------- 1 | authorityKeyIdentifier=keyid,issuer 2 | basicConstraints=CA:FALSE 3 | keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment 4 | subjectAltName = @alt_names 5 | 6 | [alt_names] 7 | DNS.1 = localhost 8 | -------------------------------------------------------------------------------- /docker/logging.conf: -------------------------------------------------------------------------------- 1 | [loggers] 2 | keys=root,sampleLogger 3 | 4 | [handlers] 5 | keys=consoleHandler 6 | 7 | [formatters] 8 | keys=sampleFormatter 9 | 10 | [logger_root] 11 | level=DEBUG 12 | handlers=consoleHandler 13 | 14 | [logger_sampleLogger] 15 | level=DEBUG 16 | handlers=consoleHandler 17 | qualname=sampleLogger 18 | propagate=0 19 | 20 | [handler_consoleHandler] 21 | class=StreamHandler 22 | level=DEBUG 23 | formatter=sampleFormatter 24 | args=(sys.stdout,) 25 | 26 | [formatter_sampleFormatter] 27 | format=%(asctime)s - %(name)s - %(levelname)s - %(message)s 28 | -------------------------------------------------------------------------------- /docker/mariadb-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eou pipefail 3 | 4 | MYSQL_DATA=/var/lib/mysql 5 | 6 | mariadb-install-db --user=mysql --basedir=/usr --datadir=$MYSQL_DATA 7 | 8 | # Start it up. 9 | mariadbd-safe --datadir=$MYSQL_DATA --skip-networking & 10 | while ! mariadb-admin ping 2>/dev/null; do 11 | sleep 1s 12 | done 13 | 14 | # Configure databases. 15 | DATABASE="aurweb" # Persistent database for fastapi. 16 | 17 | echo "Taking care of primary database '${DATABASE}'..." 18 | mariadb -u root -e "CREATE USER IF NOT EXISTS 'aur'@'localhost' IDENTIFIED BY 'aur';" 19 | mariadb -u root -e "CREATE USER IF NOT EXISTS 'aur'@'%' IDENTIFIED BY 'aur';" 20 | mariadb -u root -e "CREATE DATABASE IF NOT EXISTS $DATABASE;" 21 | 22 | mariadb -u root -e "CREATE USER IF NOT EXISTS 'aur'@'%' IDENTIFIED BY 'aur';" 23 | mariadb -u root -e "GRANT ALL ON aurweb.* TO 'aur'@'localhost';" 24 | mariadb -u root -e "GRANT ALL ON aurweb.* TO 'aur'@'%';" 25 | 26 | mariadb -u root -e "CREATE USER IF NOT EXISTS 'root'@'%' IDENTIFIED BY 'aur';" 27 | mariadb -u root -e "GRANT ALL ON *.* TO 'root'@'%' WITH GRANT OPTION;" 28 | 29 | mariadb-admin -uroot shutdown 30 | 31 | exec "$@" 32 | -------------------------------------------------------------------------------- /docker/mariadb-init-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eou pipefail 3 | 4 | # Setup a config for our mysql db. 5 | aurweb-config set database name 'aurweb' 6 | aurweb-config set database user 'aur' 7 | aurweb-config set database password 'aur' 8 | aurweb-config set database host 'localhost' 9 | aurweb-config set database socket '/var/run/mysqld/mysqld.sock' 10 | aurweb-config unset database port 11 | 12 | if [ ! -z ${NO_INITDB+x} ]; then 13 | exec "$@" 14 | fi 15 | 16 | python -m aurweb.initdb 2>/dev/null || /bin/true 17 | exec "$@" 18 | -------------------------------------------------------------------------------- /docker/nginx-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eou pipefail 3 | 4 | # If production.{cert,key}.pem exists, prefer them. This allows 5 | # user customization of the certificates that FastAPI uses. 6 | # Otherwise, fallback to localhost.{cert,key}.pem, generated by `ca`. 7 | 8 | CERT=/data/production.cert.pem 9 | KEY=/data/production.key.pem 10 | 11 | DEST_CERT=/etc/ssl/certs/web.cert.pem 12 | DEST_KEY=/etc/ssl/private/web.key.pem 13 | 14 | if [ -f "$CERT" ]; then 15 | cp -vf "$CERT" "$DEST_CERT" 16 | cp -vf "$KEY" "$DEST_KEY" 17 | else 18 | cat /data/localhost.cert.pem /data/root_ca.crt > "$DEST_CERT" 19 | cp -vf /data/localhost.key.pem "$DEST_KEY" 20 | fi 21 | 22 | cp -vf /docker/config/nginx.conf /etc/nginx/nginx.conf 23 | 24 | exec "$@" 25 | -------------------------------------------------------------------------------- /docker/redis-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eou pipefail 3 | 4 | sed -ri 's/^bind .*$/bind 0.0.0.0 -::1/g' /etc/valkey/valkey.conf 5 | sed -ri 's/protected-mode yes/protected-mode no/g' /etc/valkey/valkey.conf 6 | 7 | exec "$@" 8 | -------------------------------------------------------------------------------- /docker/scripts/install-deps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Install Arch Linux dependencies. This is centralized here 3 | # for CI and Docker usage and should always reflect the most 4 | # robust development ecosystem. 5 | set -eou pipefail 6 | 7 | # Update and rollout archlinux-keyring keys. 8 | pacman-key --init 9 | pacman-key --updatedb 10 | pacman-key --populate 11 | 12 | pacman -Sy --noconfirm --noprogressbar archlinux-keyring 13 | 14 | # Install other OS dependencies. 15 | pacman -Syu --noconfirm --noprogressbar \ 16 | git gpgme nginx valkey openssh \ 17 | mariadb mariadb-libs cgit-aurweb uwsgi uwsgi-plugin-cgi \ 18 | python-pip pyalpm python-srcinfo curl libeatmydata cronie \ 19 | python-poetry python-poetry-core step-cli step-ca asciidoc \ 20 | python-virtualenv python-pre-commit 21 | 22 | exec "$@" 23 | -------------------------------------------------------------------------------- /docker/scripts/install-python-deps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eou pipefail 3 | 4 | if [ ! -z "${COMPOSE+x}" ]; then 5 | export PIP_BREAK_SYSTEM_PACKAGES=1 6 | poetry config virtualenvs.create false 7 | fi 8 | poetry install --no-interaction --no-ansi 9 | -------------------------------------------------------------------------------- /docker/scripts/run-ca.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | STEP_DIR="$(step-cli path)" 3 | STEP_PASSWD_FILE="$STEP_DIR/password.txt" 4 | STEP_CA_CONFIG="$STEP_DIR/config/ca.json" 5 | 6 | # Start the step-ca https server. 7 | exec step-ca "$STEP_CA_CONFIG" --password-file="$STEP_PASSWD_FILE" 8 | -------------------------------------------------------------------------------- /docker/scripts/run-cgit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | exec uwsgi --socket 0.0.0.0:${1} \ 3 | --plugins cgi \ 4 | --cgi /usr/share/webapps/cgit-aurweb/cgit.cgi 5 | -------------------------------------------------------------------------------- /docker/scripts/run-cron.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd /aurweb 4 | aurweb-aurblup 5 | if [ $? -eq 0 ]; then 6 | echo "[$(date -u)] executed aurblup" >> /var/log/aurblup.log 7 | fi 8 | 9 | aurweb-mkpkglists --extended 10 | if [ $? -eq 0 ]; then 11 | echo "[$(date -u)] executed mkpkglists" >> /var/log/mkpkglists.log 12 | fi 13 | 14 | exec /usr/bin/crond -nx proc 15 | -------------------------------------------------------------------------------- /docker/scripts/run-fastapi.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # By default, set FASTAPI_WORKERS to 2. In production, this should 3 | # be configured by the deployer. 4 | if [ -z ${FASTAPI_WORKERS+x} ]; then 5 | FASTAPI_WORKERS=2 6 | fi 7 | 8 | export FASTAPI_BACKEND="$1" 9 | 10 | echo "FASTAPI_BACKEND: $FASTAPI_BACKEND" 11 | echo "FASTAPI_WORKERS: $FASTAPI_WORKERS" 12 | 13 | # Perform migrations. 14 | alembic upgrade head 15 | 16 | if [ "$1" == "uvicorn" ] || [ "$1" == "" ]; then 17 | exec uvicorn --reload \ 18 | --log-config /docker/logging.conf \ 19 | --host "0.0.0.0" \ 20 | --port 8000 \ 21 | --forwarded-allow-ips "*" \ 22 | aurweb.asgi:app 23 | elif [ "$1" == "gunicorn" ]; then 24 | exec gunicorn \ 25 | --log-config /docker/logging.conf \ 26 | --bind "0.0.0.0:8000" \ 27 | --proxy-protocol \ 28 | --forwarded-allow-ips "*" \ 29 | -w $FASTAPI_WORKERS \ 30 | -k uvicorn.workers.UvicornWorker \ 31 | aurweb.asgi:app 32 | elif [ "$1" == "hypercorn" ]; then 33 | exec hypercorn --reload \ 34 | --log-config /docker/logging.conf \ 35 | -b "0.0.0.0:8000" \ 36 | --forwarded-allow-ips "*" \ 37 | aurweb.asgi:app 38 | else 39 | echo "Error: Invalid \$FASTAPI_BACKEND supplied." 40 | echo "Valid backends: 'uvicorn', 'gunicorn', 'hypercorn'." 41 | exit 1 42 | fi 43 | -------------------------------------------------------------------------------- /docker/scripts/run-nginx.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "=== Running nginx server! ===" 4 | echo 5 | echo " Services:" 6 | echo " - FastAPI : https://localhost:8444/" 7 | echo " (cgit) : https://localhost:8444/cgit/" 8 | echo 9 | echo " Note: Copy root CA (./data/ca.root.pem) to ca-certificates or browser." 10 | echo 11 | echo " Thanks for using aurweb!" 12 | echo 13 | 14 | exec nginx -c /etc/nginx/nginx.conf 15 | -------------------------------------------------------------------------------- /docker/scripts/run-pytests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | COVERAGE=1 4 | PARAMS=() 5 | 6 | while [ $# -ne 0 ]; do 7 | key="$1" 8 | case "$key" in 9 | --no-coverage) 10 | COVERAGE=0 11 | shift 12 | ;; 13 | clean) 14 | rm -f .coverage 15 | shift 16 | ;; 17 | *) 18 | echo "usage: $0 [--no-coverage] targets ..." 19 | exit 1 20 | ;; 21 | esac 22 | done 23 | 24 | rm -rf $PROMETHEUS_MULTIPROC_DIR 25 | mkdir -p $PROMETHEUS_MULTIPROC_DIR 26 | 27 | # Run pytest with optional targets in front of it. 28 | pytest --junitxml="/data/pytest-report.xml" 29 | 30 | # By default, report coverage and move it into cache. 31 | if [ $COVERAGE -eq 1 ]; then 32 | make -C test coverage || /bin/true 33 | 34 | # /data is mounted as a volume. Copy coverage into it. 35 | # Users can then sanitize the coverage locally in their 36 | # aurweb root directory: ./util/fix-coverage ./data/.coverage 37 | rm -f /data/.coverage 38 | cp -v .coverage /data/.coverage 39 | chmod 666 /data/.coverage 40 | fi 41 | -------------------------------------------------------------------------------- /docker/scripts/run-redis.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | exec /usr/bin/redis-server /etc/valkey/valkey.conf 3 | -------------------------------------------------------------------------------- /docker/scripts/run-sharness.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eou pipefail 3 | 4 | # Initialize the new database; ignore errors. 5 | python -m aurweb.initdb 2>/dev/null || /bin/true 6 | 7 | eatmydata -- make -C test sh 8 | -------------------------------------------------------------------------------- /docker/scripts/run-smartgit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | exec uwsgi \ 4 | --socket /var/run/smartgit/smartgit.sock \ 5 | --uid root \ 6 | --gid http \ 7 | --chmod-socket=666 \ 8 | --plugins cgi \ 9 | --cgi /usr/lib/git-core/git-http-backend 10 | -------------------------------------------------------------------------------- /docker/scripts/run-sshd.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | exec /usr/sbin/sshd -e -p 2222 -D 3 | -------------------------------------------------------------------------------- /docker/scripts/run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eou pipefail 3 | dir=$(dirname $0) 4 | 5 | # Clean up coverage and stuff. 6 | make -C test clean 7 | 8 | # Run sharness tests. 9 | bash $dir/run-sharness.sh 10 | 11 | # Run Python tests with MariaDB database. 12 | # Pass --silence to avoid reporting coverage. We will do that below. 13 | bash $dir/run-pytests.sh --no-coverage 14 | 15 | make -C test coverage 16 | 17 | # /data is mounted as a volume. Copy coverage into it. 18 | # Users can then sanitize the coverage locally in their 19 | # aurweb root directory: ./util/fix-coverage ./data/.coverage 20 | rm -f /data/.coverage 21 | cp -v .coverage /data/.coverage 22 | chmod 666 /data/.coverage 23 | 24 | # Run pre-commit checks 25 | pre-commit run -a 26 | -------------------------------------------------------------------------------- /docker/scripts/update-step-config: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import json 3 | import sys 4 | 5 | CA_CONFIG = sys.argv[1] 6 | 7 | with open(CA_CONFIG) as f: 8 | data = json.load(f) 9 | 10 | if "authority" not in data: 11 | data["authority"] = dict() 12 | if "claims" not in data["authority"]: 13 | data["authority"]["claims"] = dict() 14 | 15 | # One year of certificate duration. 16 | data["authority"]["claims"] = {"maxTLSCertDuration": "8800h"} 17 | 18 | with open(CA_CONFIG, "w") as f: 19 | json.dump(data, f) 20 | -------------------------------------------------------------------------------- /docker/sharness-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eou pipefail 3 | 4 | exec "$@" 5 | -------------------------------------------------------------------------------- /docker/smartgit-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eou pipefail 3 | 4 | exec "$@" 5 | -------------------------------------------------------------------------------- /docker/test-mysql-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eou pipefail 3 | 4 | # We use the root user for testing in Docker. 5 | # The test user must be able to create databases and drop them. 6 | aurweb-config set database user 'root' 7 | aurweb-config set database host 'localhost' 8 | aurweb-config set database socket '/var/run/mysqld/mysqld.sock' 9 | 10 | # Remove possibly problematic configuration options. 11 | # We depend on the database socket within Docker and 12 | # being run as the root user. 13 | aurweb-config unset database password 14 | aurweb-config unset database port 15 | 16 | # Setup notifications for testing. 17 | aurweb-config set notifications sendmail "$(pwd)/util/sendmail" 18 | 19 | exec "$@" 20 | -------------------------------------------------------------------------------- /docker/tests-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eou pipefail 3 | dir="$(dirname $0)" 4 | 5 | bash $dir/test-mysql-entrypoint.sh 6 | 7 | exec "$@" 8 | -------------------------------------------------------------------------------- /examples/aurweb-git-auth.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Wrapper script used to call aurweb-git-update externally when 3 | # utilizing an app-based virtualenv. 4 | aurweb_dir="$HOME" 5 | cd $aurweb_dir 6 | exec poetry run aurweb-git-auth "$@" 7 | -------------------------------------------------------------------------------- /examples/aurweb-git-serve.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Wrapper script used to call aurweb-git-update externally when 3 | # utilizing an app-based virtualenv. 4 | aurweb_dir="$HOME" 5 | cd $aurweb_dir 6 | exec poetry run aurweb-git-serve "$@" 7 | -------------------------------------------------------------------------------- /examples/aurweb-git-update.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Wrapper script used to call aurweb-git-update externally when 3 | # utilizing an app-based virtualenv. 4 | aurweb_dir="$HOME" 5 | cd $aurweb_dir 6 | exec poetry run aurweb-git-update "$@" 7 | -------------------------------------------------------------------------------- /examples/aurweb.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=aurweb asgi server 3 | 4 | [Description] 5 | User=aur 6 | WorkingDirectory=/srv/http/aurweb 7 | ExecStart=/usr/bin/poetry run gunicorn \ 8 | --log-config /srv/http/aurweb/logging.conf \ 9 | --bind '0.0.0.0:8000' \ 10 | --forwarded-allow-ips '*' \ 11 | --workers 4 \ 12 | -k uvicorn.workers.UvicornWorker \ 13 | aurweb.asgi:app 14 | 15 | [Install] 16 | WantedBy=multi-user.target 17 | -------------------------------------------------------------------------------- /gunicorn.conf.py: -------------------------------------------------------------------------------- 1 | from prometheus_client import multiprocess 2 | 3 | 4 | def child_exit(server, worker): # pragma: no cover 5 | """This function is required for gunicorn customization 6 | of prometheus multiprocessing.""" 7 | multiprocess.mark_process_dead(worker.pid) 8 | -------------------------------------------------------------------------------- /logging.conf: -------------------------------------------------------------------------------- 1 | [loggers] 2 | keys=root,aurweb,uvicorn,hypercorn,alembic 3 | 4 | [handlers] 5 | keys=simpleHandler,detailedHandler 6 | 7 | [formatters] 8 | keys=simpleFormatter,detailedFormatter 9 | 10 | [logger_root] 11 | level=INFO 12 | ; We add NullHandler programmatically. 13 | handlers= 14 | propogate=0 15 | 16 | [logger_aurweb] 17 | level=INFO 18 | handlers=simpleHandler 19 | qualname=aurweb 20 | propagate=1 21 | 22 | [logger_uvicorn] 23 | level=INFO 24 | handlers=simpleHandler 25 | qualname=uvicorn 26 | propagate=0 27 | 28 | [logger_hypercorn] 29 | level=INFO 30 | handlers=simpleHandler 31 | qualname=hypercorn 32 | propagate=0 33 | 34 | [logger_alembic] 35 | level=INFO 36 | handlers=simpleHandler 37 | qualname=alembic 38 | propagate=0 39 | 40 | [handler_simpleHandler] 41 | class=StreamHandler 42 | level=DEBUG 43 | formatter=simpleFormatter 44 | args=(sys.stdout,) 45 | 46 | [handler_detailedHandler] 47 | class=StreamHandler 48 | level=DEBUG 49 | formatter=detailedFormatter 50 | args=(sys.stdout,) 51 | 52 | [formatter_simpleFormatter] 53 | format=%(asctime)s %(levelname)-8s | %(name)s @ (%(filename)s:%(lineno)d): %(message)s 54 | datefmt=%H:%M:%S 55 | 56 | [formatter_detailedFormatter] 57 | format=%(asctime)s %(levelname)-8s | [%(name)s.%(funcName)s() @ %(filename)s:%(lineno)d]: %(message)s 58 | datefmt=%H:%M:%S 59 | -------------------------------------------------------------------------------- /logging.prod.conf: -------------------------------------------------------------------------------- 1 | [loggers] 2 | keys=root,aurweb,uvicorn,hypercorn,alembic 3 | 4 | [handlers] 5 | keys=simpleHandler,detailedHandler 6 | 7 | [formatters] 8 | keys=simpleFormatter,detailedFormatter 9 | 10 | [logger_root] 11 | level=INFO 12 | ; We add NullHandler programmatically. 13 | handlers= 14 | propogate=0 15 | 16 | [logger_aurweb] 17 | level=INFO 18 | handlers=simpleHandler 19 | qualname=aurweb 20 | propagate=1 21 | 22 | [logger_uvicorn] 23 | level=WARN 24 | handlers=simpleHandler 25 | qualname=uvicorn 26 | propagate=0 27 | 28 | [logger_hypercorn] 29 | level=WARN 30 | handlers=simpleHandler 31 | qualname=hypercorn 32 | propagate=0 33 | 34 | [logger_alembic] 35 | level=WARN 36 | handlers=simpleHandler 37 | qualname=alembic 38 | propagate=0 39 | 40 | [handler_simpleHandler] 41 | class=StreamHandler 42 | level=INFO 43 | formatter=simpleFormatter 44 | args=(sys.stdout,) 45 | 46 | [handler_detailedHandler] 47 | class=StreamHandler 48 | level=DEBUG 49 | formatter=detailedFormatter 50 | args=(sys.stdout,) 51 | 52 | [formatter_simpleFormatter] 53 | format=%(asctime)s %(levelname)-8s | %(name)s @ (%(filename)s:%(lineno)d): %(message)s 54 | datefmt=%H:%M:%S 55 | 56 | [formatter_detailedFormatter] 57 | format=%(asctime)s %(levelname)-8s | [%(name)s.%(funcName)s() @ %(filename)s:%(lineno)d]: %(message)s 58 | datefmt=%H:%M:%S 59 | -------------------------------------------------------------------------------- /logging.test.conf: -------------------------------------------------------------------------------- 1 | [loggers] 2 | keys=root,aurweb,uvicorn,hypercorn,alembic 3 | 4 | [handlers] 5 | keys=simpleHandler,detailedHandler 6 | 7 | [formatters] 8 | keys=simpleFormatter,detailedFormatter 9 | 10 | [logger_root] 11 | level=INFO 12 | ; We add NullHandler programmatically. 13 | handlers= 14 | propogate=0 15 | 16 | [logger_aurweb] 17 | ; This loglevel is set to DEBUG for tests where we expect specific 18 | ; debug logging to occur. In production, this should be set to INFO. 19 | level=DEBUG 20 | handlers=simpleHandler 21 | qualname=aurweb 22 | propagate=1 23 | 24 | [logger_uvicorn] 25 | level=INFO 26 | handlers=simpleHandler 27 | qualname=uvicorn 28 | propagate=0 29 | 30 | [logger_hypercorn] 31 | level=INFO 32 | handlers=simpleHandler 33 | qualname=hypercorn 34 | propagate=0 35 | 36 | [logger_alembic] 37 | level=INFO 38 | handlers=simpleHandler 39 | qualname=alembic 40 | propagate=0 41 | 42 | [handler_simpleHandler] 43 | class=StreamHandler 44 | level=DEBUG 45 | formatter=simpleFormatter 46 | args=(sys.stdout,) 47 | 48 | [handler_detailedHandler] 49 | class=StreamHandler 50 | level=DEBUG 51 | formatter=detailedFormatter 52 | args=(sys.stdout,) 53 | 54 | [formatter_simpleFormatter] 55 | format=%(asctime)s %(levelname)-5s | %(name)s: %(message)s 56 | datefmt=%H:%M:%S 57 | 58 | [formatter_detailedFormatter] 59 | format=%(asctime)s %(levelname)-5s | %(name)s.%(funcName)s() @ L%(lineno)d: %(message)s 60 | datefmt=%H:%M:%S 61 | -------------------------------------------------------------------------------- /logs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/aurweb/ae432337466c2e38a38eaa2fdb54f193cf6a958c/logs/.gitkeep -------------------------------------------------------------------------------- /migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /migrations/versions/38e5b9982eea_add_indicies_on_packagebases_for_rss_.py: -------------------------------------------------------------------------------- 1 | """add indices on PackageBases for RSS order by 2 | 3 | Revision ID: 38e5b9982eea 4 | Revises: 7d65d35fae45 5 | Create Date: 2024-08-03 01:35:39.104283 6 | 7 | """ 8 | 9 | from alembic import op 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = "38e5b9982eea" 13 | down_revision = "7d65d35fae45" 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | # ### commands auto generated by Alembic - please adjust! ### 20 | op.create_index("BasesModifiedTS", "PackageBases", ["ModifiedTS"], unique=False) 21 | op.create_index("BasesSubmittedTS", "PackageBases", ["SubmittedTS"], unique=False) 22 | # ### end Alembic commands ### 23 | 24 | 25 | def downgrade(): 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | op.drop_index("BasesSubmittedTS", table_name="PackageBases") 28 | op.drop_index("BasesModifiedTS", table_name="PackageBases") 29 | # ### end Alembic commands ### 30 | -------------------------------------------------------------------------------- /migrations/versions/6441d3b65270_add_popularityupdated_to_packagebase.py: -------------------------------------------------------------------------------- 1 | """add PopularityUpdated to PackageBase 2 | 3 | Revision ID: 6441d3b65270 4 | Revises: d64e5571bc8d 5 | Create Date: 2022-09-22 18:08:03.280664 6 | 7 | """ 8 | 9 | from alembic import op 10 | from sqlalchemy.exc import OperationalError 11 | 12 | from aurweb.models.package_base import PackageBase 13 | from aurweb.scripts import popupdate 14 | 15 | # revision identifiers, used by Alembic. 16 | revision = "6441d3b65270" 17 | down_revision = "d64e5571bc8d" 18 | branch_labels = None 19 | depends_on = None 20 | 21 | table = PackageBase.__table__ 22 | 23 | 24 | def upgrade(): 25 | try: 26 | op.add_column(table.name, table.c.PopularityUpdated) 27 | except OperationalError: 28 | print(f"table '{table.name}' already exists, skipping migration") 29 | 30 | popupdate.run_variable() 31 | 32 | 33 | def downgrade(): 34 | op.drop_column(table.name, "PopularityUpdated") 35 | -------------------------------------------------------------------------------- /migrations/versions/6a64dd126029_rename_tu_to_package_maintainer.py: -------------------------------------------------------------------------------- 1 | """Rename TU to Package Maintainer 2 | 3 | Revision ID: 6a64dd126029 4 | Revises: c5a6a9b661a0 5 | Create Date: 2023-09-01 13:48:15.315244 6 | 7 | """ 8 | 9 | from aurweb import db 10 | from aurweb.models import AccountType 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "6a64dd126029" 14 | down_revision = "c5a6a9b661a0" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | # AccountTypes 19 | # ID 2 -> Trusted User / Package Maintainer 20 | # ID 4 -> Trusted User & Developer / Package Maintainer & Developer 21 | 22 | 23 | def upgrade(): 24 | with db.begin(): 25 | tu = db.query(AccountType).filter(AccountType.ID == 2).first() 26 | tudev = db.query(AccountType).filter(AccountType.ID == 4).first() 27 | 28 | tu.AccountType = "Package Maintainer" 29 | tudev.AccountType = "Package Maintainer & Developer" 30 | 31 | 32 | def downgrade(): 33 | with db.begin(): 34 | pm = db.query(AccountType).filter(AccountType.ID == 2).first() 35 | pmdev = db.query(AccountType).filter(AccountType.ID == 4).first() 36 | 37 | pm.AccountType = "Trusted User" 38 | pmdev.AccountType = "Trusted User & Developer" 39 | -------------------------------------------------------------------------------- /migrations/versions/7d65d35fae45_rename_tu_tables_columns.py: -------------------------------------------------------------------------------- 1 | """Rename TU tables/columns 2 | 3 | Revision ID: 7d65d35fae45 4 | Revises: 6a64dd126029 5 | Create Date: 2023-09-10 10:21:33.092342 6 | 7 | """ 8 | 9 | from alembic import op 10 | from sqlalchemy.dialects.mysql import INTEGER 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "7d65d35fae45" 14 | down_revision = "6a64dd126029" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | # TU_VoteInfo -> VoteInfo 19 | # TU_VoteInfo.ActiveTUs -> VoteInfo.ActiveUsers 20 | # TU_Votes -> Votes 21 | 22 | 23 | def upgrade(): 24 | # Tables 25 | op.rename_table("TU_VoteInfo", "VoteInfo") 26 | op.rename_table("TU_Votes", "Votes") 27 | 28 | # Columns 29 | op.alter_column( 30 | "VoteInfo", 31 | "ActiveTUs", 32 | existing_type=INTEGER(unsigned=True), 33 | new_column_name="ActiveUsers", 34 | ) 35 | 36 | 37 | def downgrade(): 38 | # Tables 39 | op.rename_table("VoteInfo", "TU_VoteInfo") 40 | op.rename_table("Votes", "TU_Votes") 41 | 42 | # Columns 43 | op.alter_column( 44 | "TU_VoteInfo", 45 | "ActiveUsers", 46 | existing_type=INTEGER(unsigned=True), 47 | new_column_name="ActiveTUs", 48 | ) 49 | -------------------------------------------------------------------------------- /migrations/versions/9e3158957fd7_add_packagekeyword_packagebaseuid.py: -------------------------------------------------------------------------------- 1 | """add PackageKeyword.PackageBaseUID index 2 | 3 | Revision ID: 9e3158957fd7 4 | Revises: 6441d3b65270 5 | Create Date: 2022-10-17 11:11:46.203322 6 | 7 | """ 8 | 9 | from alembic import op 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = "9e3158957fd7" 13 | down_revision = "6441d3b65270" 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | op.create_index( 20 | "KeywordsPackageBaseID", "PackageKeywords", ["PackageBaseID"], unique=False 21 | ) 22 | 23 | 24 | def downgrade(): 25 | op.drop_index("KeywordsPackageBaseID", table_name="PackageKeywords") 26 | -------------------------------------------------------------------------------- /migrations/versions/c5a6a9b661a0_add_index_on_packagebases_popularity_.py: -------------------------------------------------------------------------------- 1 | """Add index on PackageBases.Popularity and .Name 2 | 3 | Revision ID: c5a6a9b661a0 4 | Revises: e4e49ffce091 5 | Create Date: 2023-07-02 13:46:52.522146 6 | 7 | """ 8 | 9 | from alembic import op 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = "c5a6a9b661a0" 13 | down_revision = "e4e49ffce091" 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | op.create_index( 20 | "BasesPopularityName", "PackageBases", ["Popularity", "Name"], unique=False 21 | ) 22 | 23 | 24 | def downgrade(): 25 | op.drop_index("BasesPopularityName", table_name="PackageBases") 26 | -------------------------------------------------------------------------------- /migrations/versions/d64e5571bc8d_fix_pkgvote_votets.py: -------------------------------------------------------------------------------- 1 | """fix pkgvote votets 2 | 3 | Revision ID: d64e5571bc8d 4 | Revises: be7adae47ac3 5 | Create Date: 2022-02-18 12:47:05.322766 6 | 7 | """ 8 | 9 | from datetime import datetime 10 | 11 | import sqlalchemy as sa 12 | from alembic import op 13 | 14 | from aurweb import db 15 | from aurweb.models import PackageVote 16 | 17 | # revision identifiers, used by Alembic. 18 | revision = "d64e5571bc8d" 19 | down_revision = "be7adae47ac3" 20 | branch_labels = None 21 | depends_on = None 22 | 23 | table = PackageVote.__tablename__ 24 | column = "VoteTS" 25 | epoch = datetime(1970, 1, 1) 26 | 27 | 28 | def upgrade(): 29 | with db.begin(): 30 | records = db.query(PackageVote).filter(PackageVote.VoteTS.is_(None)) 31 | for record in records: 32 | record.VoteTS = epoch.timestamp() 33 | op.alter_column(table, column, existing_type=sa.BIGINT(), nullable=False) 34 | 35 | 36 | def downgrade(): 37 | op.alter_column(table, column, existing_type=sa.BIGINT(), nullable=True) 38 | -------------------------------------------------------------------------------- /migrations/versions/e4e49ffce091_add_hidedeletedcomments_to_user.py: -------------------------------------------------------------------------------- 1 | """Add HideDeletedComments to User 2 | 3 | Revision ID: e4e49ffce091 4 | Revises: 9e3158957fd7 5 | Create Date: 2023-04-19 23:24:25.854874 6 | 7 | """ 8 | 9 | from alembic import op 10 | from sqlalchemy.exc import OperationalError 11 | 12 | from aurweb.models.user import User 13 | 14 | # revision identifiers, used by Alembic. 15 | revision = "e4e49ffce091" 16 | down_revision = "9e3158957fd7" 17 | branch_labels = None 18 | depends_on = None 19 | 20 | table = User.__table__ 21 | 22 | 23 | def upgrade(): 24 | try: 25 | op.add_column(table.name, table.c.HideDeletedComments) 26 | except OperationalError: 27 | print( 28 | f"Column HideDeletedComments already exists in '{table.name}'," 29 | f" skipping migration." 30 | ) 31 | 32 | 33 | def downgrade(): 34 | op.drop_column(table.name, "HideDeletedComments") 35 | -------------------------------------------------------------------------------- /migrations/versions/ef39fcd6e1cd_add_sso_account_id_in_table_users.py: -------------------------------------------------------------------------------- 1 | """Add SSO account ID in table Users 2 | 3 | Revision ID: ef39fcd6e1cd 4 | Revises: f47cad5d6d03 5 | Create Date: 2020-06-08 10:04:13.898617 6 | 7 | """ 8 | 9 | import sqlalchemy as sa 10 | from alembic import op 11 | from sqlalchemy.engine.reflection import Inspector 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = "ef39fcd6e1cd" 15 | down_revision = "f47cad5d6d03" 16 | branch_labels = None 17 | depends_on = None 18 | 19 | 20 | def table_has_column(table, column_name): 21 | for element in Inspector.from_engine(op.get_bind()).get_columns(table): 22 | if element.get("name") == column_name: 23 | return True 24 | return False 25 | 26 | 27 | def upgrade(): 28 | if not table_has_column("Users", "SSOAccountID"): 29 | op.add_column( 30 | "Users", sa.Column("SSOAccountID", sa.String(length=255), nullable=True) 31 | ) 32 | op.create_unique_constraint(None, "Users", ["SSOAccountID"]) 33 | 34 | 35 | def downgrade(): 36 | if table_has_column("Users", "SSOAccountID"): 37 | op.drop_constraint("SSOAccountID", "Users", type_="unique") 38 | op.drop_column("Users", "SSOAccountID") 39 | -------------------------------------------------------------------------------- /migrations/versions/f47cad5d6d03_initial_revision.py: -------------------------------------------------------------------------------- 1 | """initial revision 2 | 3 | Revision ID: f47cad5d6d03 4 | Create Date: 2020-02-23 13:23:32.331396 5 | 6 | """ 7 | 8 | # revision identifiers, used by Alembic. 9 | revision = "f47cad5d6d03" 10 | down_revision = None 11 | branch_labels = None 12 | depends_on = None 13 | 14 | 15 | def upgrade(): 16 | pass 17 | 18 | 19 | def downgrade(): 20 | pass 21 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | filterwarnings = 3 | # This is coming from https://github.com/pytest-dev/pytest-xdist/issues/825 and it's caused from pytest-cov 4 | # Remove once fixed: https://github.com/pytest-dev/pytest-cov/issues/557 5 | ignore:The --rsyncdir command line argument and rsyncdirs config variable are deprecated.:DeprecationWarning 6 | 7 | # Build in coverage and pytest-xdist multiproc testing. 8 | addopts = --cov=aurweb --cov-append --dist load --dist loadfile -n auto 9 | 10 | # Our pytest units are located in the ./test/ directory. 11 | testpaths = test 12 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base", 5 | "group:allNonMajor" 6 | ], 7 | "packageRules": [ 8 | { 9 | "groupName": "fastapi", 10 | "matchPackageNames": ["fastapi"] 11 | }, 12 | { 13 | "matchPackageNames": ["python"], 14 | "enabled": false 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | max-complexity = 10 4 | 5 | # Ignore some unavoidable flake8 warnings; we know this is against 6 | # PEP8, but some of the existing codebase uses `I` variables, 7 | # so specifically silence warnings about it in pre-defined files. 8 | # 9 | # In E741, the 'I', 'O', 'l' are ambiguous variable names. 10 | # Our current implementation uses these variables through HTTP 11 | # and the FastAPI form specification wants them named as such. 12 | # 13 | # With {W503,W504}, PEP8 does not want us to break lines before 14 | # or after a binary operator. We have many scripts that already 15 | # do this, so we're ignoring it here. 16 | ignore = E203, E741, W503, W504 17 | -------------------------------------------------------------------------------- /static/css/archnavbar/archlogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/aurweb/ae432337466c2e38a38eaa2fdb54f193cf6a958c/static/css/archnavbar/archlogo.png -------------------------------------------------------------------------------- /static/css/archnavbar/archnavbar.css: -------------------------------------------------------------------------------- 1 | /* 2 | * ARCH GLOBAL NAVBAR 3 | * We're forcing all generic selectors with !important 4 | * to help prevent other stylesheets from interfering. 5 | */ 6 | 7 | /* container for the entire bar */ 8 | #archnavbar { min-height: 40px !important; padding: 10px 15px !important; background: #333 !important; border-bottom: 5px #08c solid !important; } 9 | #archnavbarlogo { background: url('archlogo.png') no-repeat !important; } 10 | 11 | /* move the heading/paragraph text offscreen */ 12 | #archnavbarlogo p { margin: 0 !important; padding: 0 !important; text-indent: -9999px !important; } 13 | #archnavbarlogo h1 { margin: 0 !important; padding: 0 !important; text-indent: -9999px !important; } 14 | 15 | /* make the link the same size as the logo */ 16 | #archnavbarlogo a { display: block !important; height: 40px !important; width: 190px !important; } 17 | 18 | /* display the list inline, float it to the right and style it */ 19 | [dir="rtl"] #archnavbar ul { text-align: left !important; } 20 | #archnavbar ul { display: block !important; list-style: none !important; margin: 0 !important; padding: 0 !important; font-size: 0px !important; text-align: right !important; } 21 | [dir="rtl"] #archnavbar ul li { padding: 14px 0px 0px 15px !important; } 22 | #archnavbar ul li { display: inline-block !important; font-size: 14px !important; font-family: sans-serif !important; line-height: 14px !important; padding: 14px 15px 0px !important; } 23 | 24 | /* style the links */ 25 | #archnavbar ul#archnavbarlist li a { color: #999; font-weight: bold !important; text-decoration: none !important; } 26 | #archnavbar ul li a:hover { color: white !important; text-decoration: underline !important; } 27 | -------------------------------------------------------------------------------- /static/css/archnavbar/aurlogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/aurweb/ae432337466c2e38a38eaa2fdb54f193cf6a958c/static/css/archnavbar/aurlogo.png -------------------------------------------------------------------------------- /static/html/cgit/footer.html: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /static/html/cgit/header.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 14 |
15 |
16 | -------------------------------------------------------------------------------- /static/images/ICON-LICENSE: -------------------------------------------------------------------------------- 1 | The icons used in aurweb originate from the Open Iconic project and are 2 | licensed under the following terms: 3 | 4 | ---- 5 | The MIT License (MIT) 6 | 7 | Copyright (c) 2014 Waybury 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in 17 | all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | THE SOFTWARE. 26 | ---- 27 | -------------------------------------------------------------------------------- /static/images/action-undo.min.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /static/images/action-undo.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 16 | 18 | image/svg+xml 19 | 21 | 22 | 23 | 24 | 25 | 27 | 32 | 33 | -------------------------------------------------------------------------------- /static/images/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/aurweb/ae432337466c2e38a38eaa2fdb54f193cf6a958c/static/images/ajax-loader.gif -------------------------------------------------------------------------------- /static/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/aurweb/ae432337466c2e38a38eaa2fdb54f193cf6a958c/static/images/favicon.ico -------------------------------------------------------------------------------- /static/images/pencil.min.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /static/images/pencil.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 31 | 51 | 55 | 56 | -------------------------------------------------------------------------------- /static/images/pin.min.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /static/images/pin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /static/images/rss.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /static/images/unpin.min.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /static/images/unpin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /static/images/x.min.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /static/images/x.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 16 | 18 | image/svg+xml 19 | 21 | 22 | 23 | 24 | 25 | 27 | 31 | 32 | -------------------------------------------------------------------------------- /static/js/copy.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', function() { 2 | let elements = document.querySelectorAll('.copy'); 3 | elements.forEach(function(el) { 4 | el.addEventListener('click', function(e) { 5 | e.preventDefault(); 6 | navigator.clipboard.writeText(e.target.text); 7 | }); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /static/js/typeahead-home.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', function() { 2 | const input = document.getElementById('pkgsearch-field'); 3 | const form = document.getElementById('pkgsearch-form'); 4 | const type = 'suggest'; 5 | typeahead.init(type, input, form); 6 | }); 7 | -------------------------------------------------------------------------------- /static/js/typeahead-pkgbase-merge.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', function() { 2 | const input = document.getElementById('merge_into'); 3 | const form = document.getElementById('merge-form'); 4 | const type = "suggest-pkgbase"; 5 | typeahead.init(type, input, form, false); 6 | }); 7 | -------------------------------------------------------------------------------- /static/js/typeahead-pkgbase-request.js: -------------------------------------------------------------------------------- 1 | function showHideMergeSection() { 2 | const elem = document.getElementById('id_type'); 3 | const merge_section = document.getElementById('merge_section'); 4 | if (elem.value == 'merge') { 5 | merge_section.style.display = ''; 6 | } else { 7 | merge_section.style.display = 'none'; 8 | } 9 | } 10 | 11 | function showHideRequestHints() { 12 | document.getElementById('deletion_hint').style.display = 'none'; 13 | document.getElementById('merge_hint').style.display = 'none'; 14 | document.getElementById('orphan_hint').style.display = 'none'; 15 | 16 | const elem = document.getElementById('id_type'); 17 | document.getElementById(elem.value + '_hint').style.display = ''; 18 | } 19 | 20 | document.addEventListener('DOMContentLoaded', function() { 21 | showHideMergeSection(); 22 | showHideRequestHints(); 23 | 24 | const input = document.getElementById('id_merge_into'); 25 | const form = document.getElementById('request-form'); 26 | const type = "suggest-pkgbase"; 27 | 28 | typeahead.init(type, input, form, false); 29 | }); 30 | 31 | // Bind the change event here, otherwise we have to inline javascript, 32 | // which angers CSP (Content Security Policy). 33 | document.getElementById("id_type").addEventListener("change", function() { 34 | showHideMergeSection(); 35 | showHideRequestHints(); 36 | }); 37 | -------------------------------------------------------------------------------- /static/js/typeahead-requests.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', function() { 2 | const input = document.getElementById('id_filter_pkg_name'); 3 | const form = document.getElementById('todolist_filter'); 4 | const type = 'suggest-pkgbase'; 5 | typeahead.init(type, input, form); 6 | }); 7 | -------------------------------------------------------------------------------- /templates/account/comments.html: -------------------------------------------------------------------------------- 1 | {% extends "partials/layout.html" %} 2 | 3 | {% block pageContent %} 4 |
5 |

{{ "Accounts" | tr }}

6 | 7 |
8 |
9 |

10 | {{ 11 | "Comments for %s%s%s" | tr 12 | | format('' | format(username), 13 | username, 14 | "") 15 | | safe 16 | }} 17 |

18 |
19 | 20 | 21 | 22 | 23 | {% for comment in comments %} 24 | {% include "partials/account/comment.html" %} 25 | {% endfor %} 26 | 27 |
28 | 29 |
30 | 31 | {% endblock %} 32 | -------------------------------------------------------------------------------- /templates/account/delete.html: -------------------------------------------------------------------------------- 1 | {% extends "partials/layout.html" %} 2 | 3 | {% block pageContent %} 4 |
5 |

{{ "Accounts" | tr }}

6 | 7 | {% include "partials/error.html" %} 8 | 9 |

10 | {{ 11 | "You can use this form to permanently delete the AUR account %s%s%s." 12 | | tr | format("", name, "") | safe 13 | }} 14 |

15 | 16 |

17 | {{ 18 | "%sWARNING%s: This action cannot be undone." 19 | | tr | format("", "") | safe 20 | }} 21 |

22 | 23 | 24 |
25 |
26 |

27 | 28 | 29 |

30 |

31 | 35 |

36 |

37 | 38 |

39 |
40 |
41 | 42 |
43 | {% endblock %} 44 | -------------------------------------------------------------------------------- /templates/account/edit.html: -------------------------------------------------------------------------------- 1 | {% extends "partials/layout.html" %} 2 | 3 | {% block pageContent %} 4 |
5 |

{% trans %}Accounts{% endtrans %}

6 | 7 | {% if complete %} 8 | 9 | {{ 10 | "The account, %s%s%s, has been successfully modified." 11 | | tr 12 | | format("", user.Username, "") 13 | | safe 14 | }} 15 | 16 | {% else %} 17 | {% if errors %} 18 | {% include "partials/error.html" %} 19 | {% else %} 20 |

21 | {{ "Click %shere%s if you want to permanently delete this account." 22 | | tr 23 | | format('' | format(user | account_url), 24 | "") 25 | | safe 26 | }} 27 | {{ "Click %shere%s for user details." 28 | | tr 29 | | format('' | format(user | account_url), 30 | "") 31 | | safe 32 | }} 33 | {{ "Click %shere%s to list the comments made by this account." 34 | | tr 35 | | format('' | format(user | account_url), 36 | "") 37 | | safe 38 | }} 39 |

40 | {% endif %} 41 | 42 | {% set form_type = "UpdateAccount" %} 43 | {% include "partials/account_form.html" %} 44 | {% endif %} 45 |
46 | {% endblock %} 47 | -------------------------------------------------------------------------------- /templates/account/index.html: -------------------------------------------------------------------------------- 1 | {% extends "partials/layout.html" %} 2 | 3 | {% block pageContent %} 4 |
5 |

{{ "Accounts" | tr }}

6 | 7 | {% if not users %} 8 | {{ "No results matched your search criteria." | tr }} 9 | {% else %} 10 | {% include "partials/account/results.html" %} 11 | {% endif %} 12 |
13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /templates/errors/404.html: -------------------------------------------------------------------------------- 1 | {% extends 'partials/layout.html' %} 2 | 3 | {% block pageContent %} 4 |
5 |

404 - {% trans %}Page Not Found{% endtrans %}

6 |

{% trans %}Sorry, the page you've requested does not exist.{% endtrans %}

7 | {% if pkgbase %} 8 |
    9 | {% set pkgname_strong="%s" | format(pkgbase.Name) %} 10 |
  • 11 | {% trans %}Note{% endtrans %}: 12 | {% trans %}Git clone URLs are not meant to be opened in a browser.{% endtrans %} 13 |
  • 14 |
  • 15 | {% set gitcmd="git clone %s" | format(git_clone_uri_anon | format(pkgbase.Name)) %} 16 | {% if is_maintainer %} 17 | {% set gitcmd="git clone %s" | format(git_clone_uri_priv | format(pkgbase.Name)) %} 18 | {% endif %} 19 | {{ 20 | "To clone the Git repository of %s, run %s." 21 | | tr | format(pkgname_strong, gitcmd) | safe 22 | }} 23 |
  • 24 |
  • 25 | {% set pkglink='' | format(pkgbase.Name) %} 26 | {{ 27 | "Click %shere%s to return to the %s details page." 28 | | tr | format(pkglink, "", pkgname_strong) | safe 29 | }} 30 |
  • 31 |
32 | {% endif %} 33 |
34 | {% endblock %} 35 | -------------------------------------------------------------------------------- /templates/errors/500.html: -------------------------------------------------------------------------------- 1 | {% extends "partials/layout.html" %} 2 | 3 | {% block pageContent %} 4 |
5 |

500 - {{ "Internal Server Error" | tr }}

6 | 7 |

8 | {{ "A fatal error has occurred." | tr }} 9 | {{ 10 | "Details have been logged and will be reviewed by the " 11 | "postmaster posthaste. We apologize for any inconvenience " 12 | "this may have caused." | tr 13 | }} 14 |

15 | 16 | {% if config.getboolean("options", "traceback") %} 17 |
{{ traceback }}
18 | {% endif %} 19 |
20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /templates/errors/503.html: -------------------------------------------------------------------------------- 1 | {% extends 'partials/layout.html' %} 2 | 3 | {% block pageContent %} 4 |
5 |

503 - {% trans %}Service Unavailable{% endtrans %}

6 |

{% trans %}Don't panic! This site is down due to maintenance. We will be back soon.{% endtrans %}

7 |
8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /templates/errors/detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'partials/layout.html' %} 2 | 3 | {% block pageContent %} 4 |
5 |

{{ "%d" | format(exc.status_code) }} - {{ phrase }}

6 |

{{ exc.detail }}

7 |
8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /templates/home.html: -------------------------------------------------------------------------------- 1 |
2 |

AUR {% trans %}Home{% endtrans %}

3 |

4 | {{ "Welcome to the AUR! Please read the %sAUR User Guidelines%s for more information and the %sAUR Submission Guidelines%s if you want to contribute a PKGBUILD." 5 | | tr 6 | | format('', "", 7 | '', "") 8 | | safe 9 | }} 10 | {{ "Contributed PKGBUILDs %smust%s conform to the %sArch Packaging Standards%s otherwise they will be deleted!" 11 | | tr 12 | | format("", "", 13 | '', 14 | "") 15 | | safe 16 | }} 17 | {% trans %}Remember to vote for your favourite packages!{% endtrans %} 18 | {% trans %}Some packages may be provided as binaries in [extra].{% endtrans %} 19 |

20 |

21 | {% trans %}DISCLAIMER{% endtrans %}: 22 | {% trans %}AUR packages are user produced content. Any use of the provided files is at your own risk.{% endtrans %} 23 |

24 |

{% trans %}Learn more...{% endtrans %}

25 |
26 |
27 | {% include 'partials/support.html' %} 28 |
29 | 30 | 31 | 32 | 33 | 34 | 38 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'partials/layout.html' %} 2 | 3 | {% block pageContent %} 4 |
5 |
6 | {% if request.user.is_authenticated() %} 7 | {% include 'dashboard.html' %} 8 | {% else %} 9 | {% include 'home.html' %} 10 | {% endif %} 11 |
12 |
13 |
14 | {% include 'partials/packages/search_widget.html' %} 15 | {% include 'partials/packages/updates.html' %} 16 | {% include 'partials/packages/statistics.html' %} 17 |
18 | 19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /templates/package-maintainer/index.html: -------------------------------------------------------------------------------- 1 | {% extends "partials/layout.html" %} 2 | 3 | {% block pageContent %} 4 |
5 |

{{ "Statistics" | tr }}

6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
{{ "Total" | tr }} {{ "Package Maintainers" | tr }}:{{ package_maintainer_count }}
{{ "Active" | tr }} {{ "Package Maintainers" | tr }}:{{ active_package_maintainer_count }}
18 |
19 | 20 | {% 21 | with table_class = "current-votes", 22 | total_votes = current_votes_count, 23 | results = current_votes, 24 | off_param = "coff", 25 | by_param = "cby", 26 | by_next = current_by_next, 27 | title = "Current Votes", 28 | off = current_off, 29 | by = current_by 30 | %} 31 | {% include "partials/package-maintainer/proposals.html" %} 32 | {% endwith %} 33 | 34 | {% 35 | with table_class = "past-votes", 36 | total_votes = past_votes_count, 37 | results = past_votes, 38 | off_param = "poff", 39 | by_param = "pby", 40 | by_next = past_by_next, 41 | title = "Past Votes", 42 | off = past_off, 43 | by = past_by 44 | %} 45 | {% include "partials/package-maintainer/proposals.html" %} 46 | {% endwith %} 47 | 48 | {% with title = "Last Votes by Package Maintainer", votes = last_votes_by_pm %} 49 | {% include "partials/package-maintainer/last_votes.html" %} 50 | {% endwith %} 51 | {% endblock %} 52 | -------------------------------------------------------------------------------- /templates/package-maintainer/show.html: -------------------------------------------------------------------------------- 1 | {% extends "partials/layout.html" %} 2 | 3 | {% block pageContent %} 4 |
5 | {% include "partials/package-maintainer/proposal/details.html" %} 6 |
7 | 8 | {% if utcnow >= voteinfo.End %} 9 |
10 | {% include "partials/package-maintainer/proposal/voters.html" %} 11 |
12 | {% endif %} 13 | 14 |
15 | {% if error %} 16 | {{ error | tr }} 17 | {% else %} 18 | {% include "partials/package-maintainer/proposal/form.html" %} 19 | {% endif %} 20 |
21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /templates/packages/show.html: -------------------------------------------------------------------------------- 1 | {% extends "partials/layout.html" %} 2 | 3 | {% block pageContent %} 4 | {% include "partials/packages/search.html" %} 5 |
6 |

{{ 'Package Details' | tr }}: {{ package.Name }} {{ package.Version }}

7 | 8 | {% include "partials/packages/actions.html" %} 9 | 10 | {% set show_package_details = True %} 11 | {% include "partials/packages/details.html" %} 12 | 13 |
14 | {% include "partials/packages/package_metadata.html" %} 15 |
16 |
17 | 18 | 19 | 20 | 21 | {% set pkgname = package.Name %} 22 | {% set pkgbase_id = pkgbase.ID %} 23 | {% include "partials/packages/comments.html" %} 24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /templates/partials/account/comment.html: -------------------------------------------------------------------------------- 1 | {% set header_cls = "comment-header" %} 2 | {% if comment.Deleter %} 3 | {% set header_cls = "%s %s" | format(header_cls, "comment-deleted") %} 4 | {% endif %} 5 | 6 | {% if not comment.Deleter or request.user.has_credential(creds.COMMENT_VIEW_DELETED, approved=[comment.Deleter]) %} 7 | {% if not (request.user.HideDeletedComments and comment.DelTS) %} 8 | {% set commented_at = comment.CommentTS | dt | as_timezone(timezone) %} 9 |

10 | {{ 11 | "Commented on package %s%s%s on %s%s%s" | tr 12 | | format( 13 | '' | format(comment.PackageBase.Name), 14 | comment.PackageBase.Name, 15 | "", 16 | '' | format( 17 | username, 18 | comment.ID 19 | ), 20 | datetime_display(comment.CommentTS), 21 | "" 22 | ) | safe 23 | }} 24 | {% if comment.Editor %} 25 | {% set edited_on = comment.EditedTS | dt | as_timezone(timezone) %} 26 | 27 | ({{ "edited on %s by %s" | tr 28 | | format(datetime_display(comment.EditedTS), 29 | '%s' | format( 30 | comment.Editor.Username, comment.Editor.Username)) 31 | | safe 32 | }}) 33 | 34 | {% endif %} 35 | 36 | {% include "partials/comment_actions.html" %} 37 |

38 | 39 | {% include "partials/comment_content.html" %} 40 | {% endif %} 41 | {% endif %} 42 | -------------------------------------------------------------------------------- /templates/partials/body.html: -------------------------------------------------------------------------------- 1 |
2 | {% include 'partials/set_lang.html' %} 3 | {% include 'partials/archdev-navbar.html' %} 4 | 5 | {% block pageContent %} 6 | 7 | {% endblock %} 8 | 9 | {% include 'partials/footer.html' %} 10 |
11 | -------------------------------------------------------------------------------- /templates/partials/comment_content.html: -------------------------------------------------------------------------------- 1 | 2 | {% set article_cls = "article-content" %} 3 | {% if comment.Deleter %} 4 | {% set article_cls = "%s %s" | format(article_cls, "comment-deleted") %} 5 | {% endif %} 6 | 7 |
8 |
9 | {% if comment.RenderedComment %} 10 | {{ comment.RenderedComment | safe }} 11 | {% else %} 12 | {{ comment.Comments }} 13 | {% endif %} 14 |
15 |
16 | -------------------------------------------------------------------------------- /templates/partials/error.html: -------------------------------------------------------------------------------- 1 | {% if errors %} 2 |
    3 | {% for error in errors %} 4 | {% if error is string %} 5 |
  • {{ error | tr | safe }}
  • 6 | {% elif error is iterable %} 7 |
      8 | {% for e in error %} 9 |
    • {{ e | tr | safe }}
    • 10 | {% endfor %} 11 |
    12 | {% endif %} 13 | {% endfor %} 14 |
15 | {% endif %} 16 | -------------------------------------------------------------------------------- /templates/partials/footer.html: -------------------------------------------------------------------------------- 1 | 19 | -------------------------------------------------------------------------------- /templates/partials/head.html: -------------------------------------------------------------------------------- 1 | 2 | {% include 'partials/meta.html' %} 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 15 | 16 | 17 | 18 | AUR ({{ language }}) - {{ title | tr }} 19 | 20 | -------------------------------------------------------------------------------- /templates/partials/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | {% include 'partials/head.html' %} 5 | 6 | 7 | {% include 'partials/navbar.html' %} 8 | {% extends 'partials/body.html' %} 9 | 10 | 11 | -------------------------------------------------------------------------------- /templates/partials/meta.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /templates/partials/navbar.html: -------------------------------------------------------------------------------- 1 | 20 | -------------------------------------------------------------------------------- /templates/partials/package-maintainer/last_votes.html: -------------------------------------------------------------------------------- 1 |
2 |

{% trans %}{{ title }}{% endtrans %}

3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | {% if not votes %} 12 | 13 | 16 | 17 | 18 | {% else %} 19 | {% for vote in votes %} 20 | 21 | 26 | 31 | 32 | {% endfor %} 33 | {% endif %} 34 | 35 |
{{ "User" | tr }}{{ "Last vote" | tr }}
14 | {{ "No results found." | tr }} 15 |
22 | 23 | {{ vote.Username }} 24 | 25 | 27 | 28 | {{ vote.LastVote }} 29 | 30 |
36 | 37 |
38 | -------------------------------------------------------------------------------- /templates/partials/package-maintainer/proposal/form.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 7 | 10 | 13 |
14 |
15 | -------------------------------------------------------------------------------- /templates/partials/package-maintainer/proposal/voters.html: -------------------------------------------------------------------------------- 1 |

{{ "Voters" | tr }}

2 | 11 | -------------------------------------------------------------------------------- /templates/partials/packages/pkgbase_metadata.html: -------------------------------------------------------------------------------- 1 |
2 |

Packages ({{ packages_count }})

3 | 13 |
14 | -------------------------------------------------------------------------------- /templates/partials/packages/search_actions.html: -------------------------------------------------------------------------------- 1 |

2 | 13 | 14 | 18 | 19 | 20 |

21 | -------------------------------------------------------------------------------- /templates/partials/packages/search_widget.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 6 | 12 |
13 |
14 |
15 | -------------------------------------------------------------------------------- /templates/partials/packages/updates.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | {{ "Recent Updates" | tr }} 4 | 5 | ({{ "more" | tr }}) 6 | 7 |

8 | 10 | RSS Feed 11 | 12 | 14 | RSS Feed 15 | 16 | 17 | 18 | 19 | {% for pkg in package_updates %} 20 | 21 | 26 | 29 | 30 | {% endfor %} 31 | 32 |
22 | 23 | {{ pkg.Name }} {{ pkg.Version }} 24 | 25 | 27 | {{ datetime_display(pkg.PackageBase.ModifiedTS) }} 28 |
33 | 34 |
35 | -------------------------------------------------------------------------------- /templates/partials/pager.html: -------------------------------------------------------------------------------- 1 | {# A pager widget that can be used for navigation of a number of results. 2 | 3 | Inputs required: 4 | 5 | prefix: Request URI prefix used to produce navigation offsets 6 | singular: Singular sentence to be translated via tn 7 | plural: Plural sentence to be translated via tn 8 | PP: The number of results per page 9 | O: The current offset value 10 | total: The total number of results 11 | #} 12 | 13 | {% set page = ((O / PP) | int) %} 14 | {% set pages = ((total / PP) | ceil) %} 15 | 16 |
17 |

18 | {{ total | tn(singular, plural) | format(total) }} 19 | {% if pages %} 20 | {{ "Page %d of %d." | tr | format(page + 1, pages) }} 21 | {% endif %} 22 |

23 | {% if pages > 1 %} 24 |

25 | {{ page | pager_nav(total, prefix) | safe }} 26 |

27 | {% endif %} 28 |

29 | -------------------------------------------------------------------------------- /templates/partials/set_lang.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 |
27 |
28 |
29 | -------------------------------------------------------------------------------- /templates/partials/statistics.html: -------------------------------------------------------------------------------- 1 |
2 |

{{ "My Statistics" | tr }}

3 | 4 | {% set bases = request.user.maintained_bases %} 5 | 6 | 7 | 8 | 13 | 14 | 15 | {% set out_of_date_packages = bases | out_of_date %} 16 | 17 | 22 | 23 | 24 | 25 |
9 | 10 | {{ "Packages" | tr }} 11 | 12 | {{ bases.count() }}
18 | 19 | {{ "Out of Date" | tr }} 20 | 21 | {{ out_of_date_packages.count() }}
26 | 27 |
28 | -------------------------------------------------------------------------------- /templates/pkgbase/comaintainers.html: -------------------------------------------------------------------------------- 1 | {% extends "partials/layout.html" %} 2 | 3 | {% block pageContent %} 4 | {% if errors %} 5 |
    6 | {% for error in errors %} 7 |
  • {{ error | tr }}
  • 8 | {% endfor %} 9 |
10 | {% endif %} 11 | 12 |
13 |

{{ "Manage Co-maintainers" | tr }}:

14 |

15 | {{ 16 | "Use this form to add co-maintainers for %s%s%s " 17 | "(one user name per line):" 18 | | tr | format("", pkgbase.Name, "") 19 | | safe 20 | }} 21 |

22 | 23 |
24 |
25 |

26 | 27 | 29 |

30 | 31 |

32 | 35 |

36 |
37 |
38 | 39 |
40 | {% endblock %} 41 | -------------------------------------------------------------------------------- /templates/pkgbase/flag-comment.html: -------------------------------------------------------------------------------- 1 | {% extends "partials/layout.html" %} 2 | 3 | {% block pageContent %} 4 |
5 |

{{ "Flagged Out-of-Date Comment: %s" | tr | format(pkgbase.Name) }}

6 | 7 | {# Prepare wrapping for the username. #} 8 | {% set wrap = ["", ""] %} 9 | {% if request.user.is_authenticated() %} 10 | {# When logged in, we wrap it with a link to the account. #} 11 | {% set wrap = ['' | format(pkgbase.Flagger.Username), ""] %} 12 | {% endif %} 13 | 14 | {# Prepare OutOfDateTS as a datetime object in the request user's timezone. #} 15 | {% set flagged_at = pkgbase.OutOfDateTS | dt | as_timezone(timezone) %} 16 | {% set username = "%s%s%s" | format(wrap[0], pkgbase.Flagger.Username, wrap[1]) %} 17 | 18 |

19 | {{ 20 | "%s%s%s flagged %s%s%s out-of-date on %s%s%s for the " 21 | "following reason:" 22 | | tr | format("", username, "", 23 | "", pkgbase.Name, "", 24 | "", date_display(pkgbase.OutOfDateTS), "") 25 | | safe 26 | }} 27 |

28 | 29 | {# Padding #} 30 |

31 | 32 |
33 |
34 |

{{ pkgbase.FlaggerComment }}

35 |
36 |
37 | 38 |
39 | 40 |
41 | 42 | {# Padding #} 43 |

44 | 45 |
46 | {% endblock %} 47 | -------------------------------------------------------------------------------- /templates/pkgbase/index.html: -------------------------------------------------------------------------------- 1 | {% extends "partials/layout.html" %} 2 | 3 | {% block pageContent %} 4 | {% include "partials/packages/search.html" %} 5 |
6 |

{{ 'Package Base Details' | tr }}: {{ pkgbase.Name }}

7 | 8 | {% set result = pkgbase %} 9 | {% include "partials/packages/actions.html" %} 10 | {% include "partials/packages/details.html" %} 11 | 12 |
13 | {% include "partials/packages/pkgbase_metadata.html" %} 14 |
15 |
16 | 17 | 18 | 19 | 20 | {% set pkgname = result.Name %} 21 | {% set pkgbase_id = result.ID %} 22 | {% include "partials/packages/comments.html" %} 23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /templates/pkgbase/voters.html: -------------------------------------------------------------------------------- 1 | {% extends "partials/layout.html" %} 2 | 3 | {% block pageContent %} 4 |
5 |

6 | {{ "Votes" | tr }} for 7 | 8 | {{ pkgbase.Name }} 9 | 10 |

11 | 12 |
13 | 24 |
25 |
26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /templates/register.html: -------------------------------------------------------------------------------- 1 | {% extends "partials/layout.html" %} 2 | 3 | {% block pageContent %} 4 |
5 |

{% trans %}Register{% endtrans %}

6 | 7 | {% if complete %} 8 | {{ 9 | "The account, %s%s%s, has been successfully created." 10 | | tr 11 | | format("", "'" + user.Username + "'", "") 12 | | safe 13 | }} 14 |

15 | {% trans %}A password reset key has been sent to your e-mail address.{% endtrans %} 16 |

17 | {% else %} 18 | {% if errors %} 19 | {% include "partials/error.html" %} 20 | {% else %} 21 |

22 | {% trans %}Use this form to create an account.{% endtrans %} 23 |

24 | {% endif %} 25 | 26 | {% set form_type = "NewAccount" %} 27 | {% include "partials/account_form.html" %} 28 | {% endif %} 29 |
30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /templates/requests/close.html: -------------------------------------------------------------------------------- 1 | {% extends "partials/layout.html" %} 2 | 3 | {% block pageContent %} 4 |
5 |

{{ "Close Request" | tr }}: {{ pkgreq.PackageBaseName }}

6 | 7 |

8 | {{ 9 | "Use this form to close the request for package base %s%s%s." 10 | | tr | format("", pkgreq.PackageBaseName, "") 11 | | safe 12 | }} 13 |

14 | 15 |

16 | {{ "Note" | tr }}: 17 | {{ 18 | "The comments field can be left empty. However, it is highly " 19 | "recommended to add a comment when rejecting a request." 20 | | tr 21 | }} 22 |

23 | 24 |
25 |
26 |

27 | 28 | 31 |

32 | 33 |

34 | 37 |

38 | 39 |
40 |
41 | 42 |
43 | {% endblock %} 44 | -------------------------------------------------------------------------------- /templates/testing/PKGBUILD.j2: -------------------------------------------------------------------------------- 1 | pkgname={{ pkg.PackageBase.Name }} 2 | pkgver={{ pkg.Version }} 3 | pkgrel=1 4 | pkgdesc='{{ pkg.Description }}' 5 | url='{{ pkg.URL }}' 6 | arch='any' 7 | license=({{ licenses | join(" ") }}) 8 | depends=({{ depends | join(" ") }}) 9 | source=() 10 | md5sums=() 11 | 12 | package() { 13 | {{ body }} 14 | } 15 | -------------------------------------------------------------------------------- /templates/testing/SRCINFO.j2: -------------------------------------------------------------------------------- 1 | pkgbase = {{ pkg.PackageBase.name }} 2 | pkgver = {{ pkg.Version }} 3 | pkgrel = 1 4 | pkgdesc = {{ pkg.Description }} 5 | url = {{ pkg.URL }} 6 | arch='any' 7 | license = {{ pkg.package_licenses | join(", ", attribute="License.Name") }} 8 | depends = {{ pkg.package_dependencies | join(", ", attribute="DepName") }} 9 | 10 | pkgname = {{ pkg.Name }} 11 | -------------------------------------------------------------------------------- /templates/testing/alpm_package.j2: -------------------------------------------------------------------------------- 1 | %FILENAME% 2 | {{ pkgname }}-{{ pkgver }}-{{ arch }}.pkg.tar.xz 3 | 4 | %NAME% 5 | {{ pkgname }} 6 | 7 | %VERSION% 8 | {{ pkgver }}-1 9 | 10 | %ARCH% 11 | {{ arch }} 12 | 13 | {% if provides %} 14 | %PROVIDES% 15 | {{ provides | join("\n") }} 16 | {% endif %} 17 | -------------------------------------------------------------------------------- /test/Makefile: -------------------------------------------------------------------------------- 1 | T = $(sort $(wildcard *.t)) 2 | 3 | PROVE := $(shell command -v prove 2> /dev/null) 4 | 5 | MAKEFLAGS = -j1 6 | 7 | # IMPORTANT: `sh` should come somewhere AFTER `pytest`. 8 | check: sh pytest 9 | 10 | pytest: 11 | cd .. && coverage run --append /usr/bin/pytest test 12 | 13 | ifdef PROVE 14 | sh: 15 | prove . 16 | else 17 | sh: $(T) 18 | endif 19 | 20 | coverage: 21 | cd .. && coverage report --include='aurweb/*' 22 | cd .. && coverage xml --include='aurweb/*' 23 | 24 | clean: 25 | $(RM) -r test-results/ 26 | rm -f ../.coverage 27 | 28 | $(T): 29 | @echo "*** $@ ***"; $(SHELL) $@ -v 30 | 31 | .PHONY: check coverage $(FOREIGN_TARGETS) clean $(T) 32 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/aurweb/ae432337466c2e38a38eaa2fdb54f193cf6a958c/test/__init__.py -------------------------------------------------------------------------------- /test/scripts/cover: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # This script is used by sharness tests hosted in our `test` 3 | # directory. We require a concrete script to make using this easily, 4 | # because we often call `env` in those tests. 5 | # 6 | # The purpose of this script is to allow sharness tests to gather 7 | # Python coverage when calling scripts within `aurweb`. 8 | # 9 | TOPLEVEL=$(dirname "$0")/../.. 10 | 11 | # Define a COVERAGE_FILE in our root directory. 12 | COVERAGE_FILE="$TOPLEVEL/.coverage" \ 13 | coverage run -L --source="$TOPLEVEL/aurweb" --append "$@" 14 | -------------------------------------------------------------------------------- /test/t1100-git-auth.t: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | test_description='git-auth tests' 4 | 5 | . "$(dirname "$0")/setup.sh" 6 | 7 | 8 | test_expect_success 'Test basic authentication.' ' 9 | cover "$GIT_AUTH" "$AUTH_KEYTYPE_USER" "$AUTH_KEYTEXT_USER" >out && 10 | grep -q AUR_USER=user out && 11 | grep -q AUR_PRIVILEGED=0 out 12 | ' 13 | 14 | test_expect_success 'Test Package Maintainer authentication.' ' 15 | cover "$GIT_AUTH" "$AUTH_KEYTYPE_PM" "$AUTH_KEYTEXT_PM" >out && 16 | grep -q AUR_USER=pm out && 17 | grep -q AUR_PRIVILEGED=1 out 18 | ' 19 | 20 | test_expect_success 'Test authentication with an unsupported key type.' ' 21 | test_must_fail cover "$GIT_AUTH" ssh-xxx "$AUTH_KEYTEXT_USER" 22 | ' 23 | 24 | test_expect_success 'Test authentication with a wrong key.' ' 25 | cover "$GIT_AUTH" "$AUTH_KEYTYPE_MISSING" "$AUTH_KEYTEXT_MISSING" >out 26 | test_must_be_empty out 27 | ' 28 | 29 | test_done 30 | -------------------------------------------------------------------------------- /test/test_accepted_term.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from sqlalchemy.exc import IntegrityError 3 | 4 | from aurweb import db 5 | from aurweb.models.accepted_term import AcceptedTerm 6 | from aurweb.models.account_type import USER_ID 7 | from aurweb.models.term import Term 8 | from aurweb.models.user import User 9 | 10 | 11 | @pytest.fixture(autouse=True) 12 | def setup(db_test): 13 | return 14 | 15 | 16 | @pytest.fixture 17 | def user() -> User: 18 | with db.begin(): 19 | user = db.create( 20 | User, 21 | Username="test", 22 | Email="test@example.org", 23 | RealName="Test User", 24 | Passwd="testPassword", 25 | AccountTypeID=USER_ID, 26 | ) 27 | yield user 28 | 29 | 30 | @pytest.fixture 31 | def term() -> Term: 32 | with db.begin(): 33 | term = db.create(Term, Description="Test term", URL="https://test.term") 34 | yield term 35 | 36 | 37 | @pytest.fixture 38 | def accepted_term(user: User, term: Term) -> AcceptedTerm: 39 | with db.begin(): 40 | accepted_term = db.create(AcceptedTerm, User=user, Term=term) 41 | yield accepted_term 42 | 43 | 44 | def test_accepted_term(user: User, term: Term, accepted_term: AcceptedTerm): 45 | # Make sure our AcceptedTerm relationships got initialized properly. 46 | assert accepted_term.User == user 47 | assert accepted_term in user.accepted_terms 48 | assert accepted_term in term.accepted_terms 49 | 50 | 51 | def test_accepted_term_null_user_raises_exception(term: Term): 52 | with pytest.raises(IntegrityError): 53 | AcceptedTerm(Term=term) 54 | 55 | 56 | def test_accepted_term_null_term_raises_exception(user: User): 57 | with pytest.raises(IntegrityError): 58 | AcceptedTerm(User=user) 59 | -------------------------------------------------------------------------------- /test/test_account_type.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from aurweb import db 4 | from aurweb.models.account_type import AccountType 5 | from aurweb.models.user import User 6 | 7 | 8 | @pytest.fixture(autouse=True) 9 | def setup(db_test): 10 | return 11 | 12 | 13 | @pytest.fixture 14 | def account_type() -> AccountType: 15 | with db.begin(): 16 | account_type_ = db.create(AccountType, AccountType="TestUser") 17 | 18 | yield account_type_ 19 | 20 | with db.begin(): 21 | db.delete(account_type_) 22 | 23 | 24 | def test_account_type(account_type): 25 | """Test creating an AccountType, and reading its columns.""" 26 | # Make sure it got db.created and was given an ID. 27 | assert bool(account_type.ID) 28 | 29 | # Next, test our string functions. 30 | assert str(account_type) == "TestUser" 31 | assert repr(account_type) == "" % ( 32 | account_type.ID 33 | ) 34 | 35 | record = db.query(AccountType, AccountType.AccountType == "TestUser").first() 36 | assert account_type == record 37 | 38 | 39 | def test_user_account_type_relationship(account_type): 40 | with db.begin(): 41 | user = db.create( 42 | User, 43 | Username="test", 44 | Email="test@example.org", 45 | RealName="Test User", 46 | Passwd="testPassword", 47 | AccountType=account_type, 48 | ) 49 | 50 | assert user.AccountType == account_type 51 | 52 | # This must be db.deleted here to avoid foreign key issues when 53 | # deleting the temporary AccountType in the fixture. 54 | with db.begin(): 55 | db.delete(user) 56 | -------------------------------------------------------------------------------- /test/test_api_rate_limit.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from sqlalchemy.exc import IntegrityError 3 | 4 | from aurweb import db 5 | from aurweb.models.api_rate_limit import ApiRateLimit 6 | 7 | 8 | @pytest.fixture(autouse=True) 9 | def setup(db_test): 10 | return 11 | 12 | 13 | def test_api_rate_key_creation(): 14 | with db.begin(): 15 | rate = db.create(ApiRateLimit, IP="127.0.0.1", Requests=10, WindowStart=1) 16 | assert rate.IP == "127.0.0.1" 17 | assert rate.Requests == 10 18 | assert rate.WindowStart == 1 19 | 20 | 21 | def test_api_rate_key_ip_default(): 22 | with db.begin(): 23 | api_rate_limit = db.create(ApiRateLimit, Requests=10, WindowStart=1) 24 | assert api_rate_limit.IP == str() 25 | 26 | 27 | def test_api_rate_key_null_requests_raises_exception(): 28 | with pytest.raises(IntegrityError): 29 | ApiRateLimit(IP="127.0.0.1", WindowStart=1) 30 | 31 | 32 | def test_api_rate_key_null_window_start_raises_exception(): 33 | with pytest.raises(IntegrityError): 34 | ApiRateLimit(IP="127.0.0.1", Requests=1) 35 | -------------------------------------------------------------------------------- /test/test_ban.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | from datetime import UTC, datetime, timedelta 3 | 4 | import pytest 5 | from sqlalchemy import exc as sa_exc 6 | 7 | from aurweb import db 8 | from aurweb.db import create 9 | from aurweb.models.ban import Ban, is_banned 10 | from aurweb.testing.requests import Request 11 | 12 | 13 | @pytest.fixture(autouse=True) 14 | def setup(db_test): 15 | return 16 | 17 | 18 | @pytest.fixture 19 | def ban() -> Ban: 20 | ts = datetime.now(UTC) + timedelta(seconds=30) 21 | with db.begin(): 22 | ban = create(Ban, IPAddress="127.0.0.1", BanTS=ts) 23 | yield ban 24 | 25 | 26 | def test_ban(ban: Ban): 27 | assert ban.IPAddress == "127.0.0.1" 28 | assert bool(ban.BanTS) 29 | 30 | 31 | def test_invalid_ban(): 32 | with pytest.raises(sa_exc.IntegrityError): 33 | bad_ban = Ban(BanTS=datetime.now(UTC)) 34 | 35 | # We're adding a ban with no primary key; this causes an 36 | # SQLAlchemy warnings when committing to the DB. 37 | # Ignore them. 38 | with warnings.catch_warnings(): 39 | warnings.simplefilter("ignore", sa_exc.SAWarning) 40 | with db.begin(): 41 | db.add(bad_ban) 42 | 43 | # Since we got a transaction failure, we need to rollback. 44 | db.rollback() 45 | 46 | 47 | def test_banned(ban: Ban): 48 | request = Request() 49 | request.client.host = "127.0.0.1" 50 | assert is_banned(request) 51 | 52 | 53 | def test_not_banned(ban: Ban): 54 | request = Request() 55 | request.client.host = "192.168.0.1" 56 | assert not is_banned(request) 57 | -------------------------------------------------------------------------------- /test/test_defaults.py: -------------------------------------------------------------------------------- 1 | from aurweb import defaults 2 | 3 | 4 | def test_fallback_pp(): 5 | assert defaults.fallback_pp(75) == defaults.PP 6 | assert defaults.fallback_pp(100) == 100 7 | 8 | 9 | def test_pp(): 10 | assert defaults.PP == 50 11 | 12 | 13 | def test_o(): 14 | assert defaults.O == 0 15 | -------------------------------------------------------------------------------- /test/test_dependency_type.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from aurweb.db import begin, create, delete, query 4 | from aurweb.models.dependency_type import DependencyType 5 | 6 | 7 | @pytest.fixture(autouse=True) 8 | def setup(db_test): 9 | return 10 | 11 | 12 | def test_dependency_types(): 13 | dep_types = ["depends", "makedepends", "checkdepends", "optdepends"] 14 | for dep_type in dep_types: 15 | dependency_type = query(DependencyType, DependencyType.Name == dep_type).first() 16 | assert dependency_type is not None 17 | 18 | 19 | def test_dependency_type_creation(): 20 | with begin(): 21 | dependency_type = create(DependencyType, Name="Test Type") 22 | assert bool(dependency_type.ID) 23 | assert dependency_type.Name == "Test Type" 24 | with begin(): 25 | delete(dependency_type) 26 | 27 | 28 | def test_dependency_type_null_name_uses_default(): 29 | with begin(): 30 | dependency_type = create(DependencyType) 31 | assert dependency_type.Name == str() 32 | with begin(): 33 | delete(dependency_type) 34 | -------------------------------------------------------------------------------- /test/test_email.py: -------------------------------------------------------------------------------- 1 | import io 2 | from subprocess import PIPE, Popen 3 | 4 | import pytest 5 | 6 | from aurweb import config 7 | from aurweb.testing.email import Email 8 | 9 | 10 | @pytest.fixture(autouse=True) 11 | def setup(email_test): 12 | return 13 | 14 | 15 | def sendmail(from_: str, to_: str, content: str) -> Email: 16 | binary = config.get("notifications", "sendmail") 17 | proc = Popen(binary, stdin=PIPE, stdout=PIPE, stderr=PIPE) 18 | content = f"From: {from_}\nTo: {to_}\n\n{content}" 19 | proc.communicate(content.encode()) 20 | proc.wait() 21 | assert proc.returncode == 0 22 | 23 | 24 | def test_email_glue(): 25 | """Test that Email.glue() decodes both base64 and decoded content.""" 26 | body = "Test email." 27 | sendmail("test@example.org", "test@example.org", body) 28 | assert Email.count() == 1 29 | 30 | email1 = Email(1) 31 | email2 = Email(1) 32 | assert email1.glue() == email2.glue() 33 | 34 | 35 | def test_email_dump(): 36 | """Test that Email.dump() dumps a single email.""" 37 | body = "Test email." 38 | sendmail("test@example.org", "test@example.org", body) 39 | assert Email.count() == 1 40 | 41 | stdout = io.StringIO() 42 | Email.dump(file=stdout) 43 | content = stdout.getvalue() 44 | assert "== Email #1 ==" in content 45 | 46 | 47 | def test_email_dump_multiple(): 48 | """Test that Email.dump() dumps multiple emails.""" 49 | body = "Test email." 50 | sendmail("test@example.org", "test@example.org", body) 51 | sendmail("test2@example.org", "test2@example.org", body) 52 | assert Email.count() == 2 53 | 54 | stdout = io.StringIO() 55 | Email.dump(file=stdout) 56 | content = stdout.getvalue() 57 | assert "== Email #1 ==" in content 58 | assert "== Email #2 ==" in content 59 | -------------------------------------------------------------------------------- /test/test_filelock.py: -------------------------------------------------------------------------------- 1 | import py 2 | from _pytest.logging import LogCaptureFixture 3 | 4 | from aurweb.testing.filelock import FileLock 5 | 6 | 7 | def test_filelock(tmpdir: py.path.local): 8 | cb_path = None 9 | 10 | def setup(path: str): 11 | nonlocal cb_path 12 | cb_path = str(path) 13 | 14 | flock = FileLock(tmpdir, "test") 15 | assert not flock.lock(on_create=setup) 16 | assert cb_path == str(tmpdir / "test") 17 | assert flock.lock() 18 | 19 | 20 | def test_filelock_default(caplog: LogCaptureFixture, tmpdir: py.path.local): 21 | # Test default_on_create here. 22 | flock = FileLock(tmpdir, "test") 23 | assert not flock.lock() 24 | assert caplog.messages[0] == f"Filelock at {flock.path} acquired." 25 | assert flock.lock() 26 | -------------------------------------------------------------------------------- /test/test_filters.py: -------------------------------------------------------------------------------- 1 | from datetime import UTC, datetime 2 | from zoneinfo import ZoneInfo 3 | 4 | import pytest 5 | 6 | from aurweb import filters, time 7 | 8 | 9 | def test_timestamp_to_datetime(): 10 | ts = time.utcnow() 11 | dt = datetime.fromtimestamp(ts, UTC) 12 | assert filters.timestamp_to_datetime(ts) == dt 13 | 14 | 15 | def test_as_timezone(): 16 | ts = time.utcnow() 17 | dt = filters.timestamp_to_datetime(ts) 18 | assert filters.as_timezone(dt, "UTC") == dt.astimezone(tz=ZoneInfo("UTC")) 19 | 20 | 21 | def test_number_format(): 22 | assert filters.number_format(0.222, 2) == "0.22" 23 | assert filters.number_format(0.226, 2) == "0.23" 24 | 25 | 26 | def test_extend_query(): 27 | """Test extension of a query via extend_query.""" 28 | query = {"a": "b"} 29 | extended = filters.extend_query(query, ("a", "c"), ("b", "d")) 30 | assert extended.get("a") == "c" 31 | assert extended.get("b") == "d" 32 | 33 | 34 | def test_to_qs(): 35 | """Test conversion from a query dictionary to a query string.""" 36 | query = {"a": "b", "c": [1, 2, 3]} 37 | qs = filters.to_qs(query) 38 | assert qs == "a=b&c=1&c=2&c=3" 39 | 40 | 41 | @pytest.mark.parametrize( 42 | "value, args, expected", 43 | [ 44 | ("", (), ""), 45 | ("a", (), "a"), 46 | ("a", (1,), "a"), 47 | ("%s", ("a",), "a"), 48 | ("%s", ("ab",), "ab"), 49 | ("%s%d", ("a", 1), "a1"), 50 | ], 51 | ) 52 | def test_safe_format(value: str, args: tuple, expected: str): 53 | assert filters.safe_format(value, *args) == expected 54 | -------------------------------------------------------------------------------- /test/test_group.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from sqlalchemy.exc import IntegrityError 3 | 4 | from aurweb import db 5 | from aurweb.models.group import Group 6 | 7 | 8 | @pytest.fixture(autouse=True) 9 | def setup(db_test): 10 | return 11 | 12 | 13 | def test_group_creation(): 14 | with db.begin(): 15 | group = db.create(Group, Name="Test Group") 16 | assert bool(group.ID) 17 | assert group.Name == "Test Group" 18 | 19 | 20 | def test_group_null_name_raises_exception(): 21 | with pytest.raises(IntegrityError): 22 | Group() 23 | -------------------------------------------------------------------------------- /test/test_initdb.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import aurweb.config 4 | import aurweb.db 5 | import aurweb.initdb 6 | from aurweb.models.account_type import AccountType 7 | 8 | 9 | @pytest.fixture(autouse=True) 10 | def setup(db_test): 11 | return 12 | 13 | 14 | class Args: 15 | use_alembic = True 16 | verbose = True 17 | 18 | 19 | def test_run(): 20 | from aurweb.schema import metadata 21 | 22 | aurweb.db.kill_engine() 23 | metadata.drop_all(aurweb.db.get_engine()) 24 | aurweb.initdb.run(Args()) 25 | 26 | # Check that constant table rows got added via initdb. 27 | record = aurweb.db.query(AccountType, AccountType.AccountType == "User").first() 28 | assert record is not None 29 | -------------------------------------------------------------------------------- /test/test_license.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from sqlalchemy.exc import IntegrityError 3 | 4 | from aurweb import db 5 | from aurweb.models.license import License 6 | 7 | 8 | @pytest.fixture(autouse=True) 9 | def setup(db_test): 10 | return 11 | 12 | 13 | def test_license_creation(): 14 | with db.begin(): 15 | license = db.create(License, Name="Test License") 16 | assert bool(license.ID) 17 | assert license.Name == "Test License" 18 | 19 | 20 | def test_license_null_name_raises_exception(): 21 | with pytest.raises(IntegrityError): 22 | License() 23 | -------------------------------------------------------------------------------- /test/test_logging.py: -------------------------------------------------------------------------------- 1 | from aurweb import aur_logging 2 | 3 | logger = aur_logging.get_logger(__name__) 4 | 5 | 6 | def test_logging(caplog): 7 | logger.info("Test log.") 8 | 9 | # Test that we logged once. 10 | assert len(caplog.records) == 1 11 | 12 | # Test that our log record was of INFO level. 13 | assert caplog.records[0].levelname == "INFO" 14 | 15 | # Test that our message got logged. 16 | assert "Test log." in caplog.text 17 | -------------------------------------------------------------------------------- /test/test_metrics.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from prometheus_client import REGISTRY, generate_latest 3 | 4 | from aurweb import db 5 | from aurweb.cache import db_query_cache 6 | from aurweb.models.account_type import USER_ID 7 | from aurweb.models.user import User 8 | 9 | 10 | @pytest.fixture(autouse=True) 11 | def setup(db_test, prometheus_test): 12 | return 13 | 14 | 15 | @pytest.fixture 16 | def user() -> User: 17 | with db.begin(): 18 | user = db.create( 19 | User, 20 | Username="test", 21 | Email="test@example.org", 22 | RealName="Test User", 23 | Passwd="testPassword", 24 | AccountTypeID=USER_ID, 25 | ) 26 | yield user 27 | 28 | 29 | def test_search_cache_metrics(user: User): 30 | # Fire off 3 identical queries for caching 31 | for _ in range(3): 32 | db_query_cache("key", db.query(User)) 33 | 34 | # Get metrics 35 | metrics = str(generate_latest(REGISTRY)) 36 | 37 | # We should have 1 miss and 2 hits 38 | assert 'search_requests_total{cache="miss"} 1.0' in metrics 39 | assert 'search_requests_total{cache="hit"} 2.0' in metrics 40 | -------------------------------------------------------------------------------- /test/test_package_blacklist.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from sqlalchemy.exc import IntegrityError 3 | 4 | from aurweb import db 5 | from aurweb.models.package_blacklist import PackageBlacklist 6 | 7 | 8 | @pytest.fixture(autouse=True) 9 | def setup(db_test): 10 | return 11 | 12 | 13 | def test_package_blacklist_creation(): 14 | with db.begin(): 15 | package_blacklist = db.create(PackageBlacklist, Name="evil-package") 16 | assert bool(package_blacklist.ID) 17 | assert package_blacklist.Name == "evil-package" 18 | 19 | 20 | def test_package_blacklist_null_name_raises_exception(): 21 | with pytest.raises(IntegrityError): 22 | PackageBlacklist() 23 | -------------------------------------------------------------------------------- /test/test_package_keyword.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from sqlalchemy.exc import IntegrityError 3 | 4 | from aurweb import db 5 | from aurweb.models.account_type import USER_ID 6 | from aurweb.models.package_base import PackageBase 7 | from aurweb.models.package_keyword import PackageKeyword 8 | from aurweb.models.user import User 9 | 10 | 11 | @pytest.fixture(autouse=True) 12 | def setup(db_test): 13 | return 14 | 15 | 16 | @pytest.fixture 17 | def user() -> User: 18 | with db.begin(): 19 | user = db.create( 20 | User, 21 | Username="test", 22 | Email="test@example.org", 23 | RealName="Test User", 24 | Passwd="testPassword", 25 | AccountTypeID=USER_ID, 26 | ) 27 | yield user 28 | 29 | 30 | @pytest.fixture 31 | def pkgbase(user: User) -> PackageBase: 32 | with db.begin(): 33 | pkgbase = db.create(PackageBase, Name="beautiful-package", Maintainer=user) 34 | yield pkgbase 35 | 36 | 37 | def test_package_keyword(pkgbase: PackageBase): 38 | with db.begin(): 39 | pkg_keyword = db.create(PackageKeyword, PackageBase=pkgbase, Keyword="test") 40 | assert pkg_keyword in pkgbase.keywords 41 | assert pkgbase == pkg_keyword.PackageBase 42 | 43 | 44 | def test_package_keyword_null_pkgbase_raises_exception(): 45 | with pytest.raises(IntegrityError): 46 | PackageKeyword(Keyword="test") 47 | -------------------------------------------------------------------------------- /test/test_package_notification.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from sqlalchemy.exc import IntegrityError 3 | 4 | from aurweb import db 5 | from aurweb.models.package_base import PackageBase 6 | from aurweb.models.package_notification import PackageNotification 7 | from aurweb.models.user import User 8 | 9 | 10 | @pytest.fixture(autouse=True) 11 | def setup(db_test): 12 | return 13 | 14 | 15 | @pytest.fixture 16 | def user() -> User: 17 | with db.begin(): 18 | user = db.create( 19 | User, 20 | Username="test", 21 | Email="test@example.org", 22 | RealName="Test User", 23 | Passwd="testPassword", 24 | ) 25 | yield user 26 | 27 | 28 | @pytest.fixture 29 | def pkgbase(user: User) -> PackageBase: 30 | with db.begin(): 31 | pkgbase = db.create(PackageBase, Name="test-package", Maintainer=user) 32 | yield pkgbase 33 | 34 | 35 | def test_package_notification_creation(user: User, pkgbase: PackageBase): 36 | with db.begin(): 37 | package_notification = db.create( 38 | PackageNotification, User=user, PackageBase=pkgbase 39 | ) 40 | assert bool(package_notification) 41 | assert package_notification.User == user 42 | assert package_notification.PackageBase == pkgbase 43 | 44 | 45 | def test_package_notification_null_user_raises(pkgbase: PackageBase): 46 | with pytest.raises(IntegrityError): 47 | PackageNotification(PackageBase=pkgbase) 48 | 49 | 50 | def test_package_notification_null_pkgbase_raises(user: User): 51 | with pytest.raises(IntegrityError): 52 | PackageNotification(User=user) 53 | -------------------------------------------------------------------------------- /test/test_package_source.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from sqlalchemy.exc import IntegrityError 3 | 4 | from aurweb import db 5 | from aurweb.models.account_type import USER_ID 6 | from aurweb.models.package import Package 7 | from aurweb.models.package_base import PackageBase 8 | from aurweb.models.package_source import PackageSource 9 | from aurweb.models.user import User 10 | 11 | 12 | @pytest.fixture(autouse=True) 13 | def setup(db_test): 14 | return 15 | 16 | 17 | @pytest.fixture 18 | def user() -> User: 19 | with db.begin(): 20 | user = db.create( 21 | User, 22 | Username="test", 23 | Email="test@example.org", 24 | RealName="Test User", 25 | Passwd="testPassword", 26 | AccountTypeID=USER_ID, 27 | ) 28 | yield user 29 | 30 | 31 | @pytest.fixture 32 | def package(user: User) -> Package: 33 | with db.begin(): 34 | pkgbase = db.create(PackageBase, Name="test-package", Maintainer=user) 35 | package = db.create(Package, PackageBase=pkgbase, Name="test-package") 36 | yield package 37 | 38 | 39 | def test_package_source(package: Package): 40 | with db.begin(): 41 | pkgsource = db.create(PackageSource, Package=package) 42 | assert pkgsource.Package == package 43 | # By default, PackageSources.Source assigns the string '/dev/null'. 44 | assert pkgsource.Source == "/dev/null" 45 | assert pkgsource.SourceArch is None 46 | 47 | 48 | def test_package_source_null_package_raises(): 49 | with pytest.raises(IntegrityError): 50 | PackageSource() 51 | -------------------------------------------------------------------------------- /test/test_pm_vote.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from sqlalchemy.exc import IntegrityError 3 | 4 | from aurweb import db, time 5 | from aurweb.models.account_type import PACKAGE_MAINTAINER_ID 6 | from aurweb.models.user import User 7 | from aurweb.models.vote import Vote 8 | from aurweb.models.voteinfo import VoteInfo 9 | 10 | 11 | @pytest.fixture(autouse=True) 12 | def setup(db_test): 13 | return 14 | 15 | 16 | @pytest.fixture 17 | def user() -> User: 18 | with db.begin(): 19 | user = db.create( 20 | User, 21 | Username="test", 22 | Email="test@example.org", 23 | RealName="Test User", 24 | Passwd="testPassword", 25 | AccountTypeID=PACKAGE_MAINTAINER_ID, 26 | ) 27 | yield user 28 | 29 | 30 | @pytest.fixture 31 | def voteinfo(user: User) -> VoteInfo: 32 | ts = time.utcnow() 33 | with db.begin(): 34 | voteinfo = db.create( 35 | VoteInfo, 36 | Agenda="Blah blah.", 37 | User=user.Username, 38 | Submitted=ts, 39 | End=ts + 5, 40 | Quorum=0.5, 41 | Submitter=user, 42 | ) 43 | yield voteinfo 44 | 45 | 46 | def test_vote_creation(user: User, voteinfo: VoteInfo): 47 | with db.begin(): 48 | vote = db.create(Vote, User=user, VoteInfo=voteinfo) 49 | 50 | assert vote.VoteInfo == voteinfo 51 | assert vote.User == user 52 | assert vote in user.votes 53 | assert vote in voteinfo.votes 54 | 55 | 56 | def test_vote_null_user_raises_exception(voteinfo: VoteInfo): 57 | with pytest.raises(IntegrityError): 58 | Vote(VoteInfo=voteinfo) 59 | 60 | 61 | def test_vote_null_voteinfo_raises_exception(user: User): 62 | with pytest.raises(IntegrityError): 63 | Vote(User=user) 64 | -------------------------------------------------------------------------------- /test/test_popupdate.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from aurweb.scripts import popupdate 4 | 5 | 6 | @pytest.fixture(autouse=True) 7 | def setup(db_test): 8 | return 9 | 10 | 11 | def test_popupdate(): 12 | popupdate.main() 13 | -------------------------------------------------------------------------------- /test/test_redis.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | import pytest 4 | 5 | import aurweb.config 6 | from aurweb.aur_redis import redis_connection 7 | 8 | 9 | @pytest.fixture 10 | def redis(): 11 | """Create a RedisStub.""" 12 | 13 | def mock_get(section, key): 14 | return "none" 15 | 16 | with mock.patch("aurweb.config.get", side_effect=mock_get): 17 | aurweb.config.rehash() 18 | redis = redis_connection() 19 | aurweb.config.rehash() 20 | 21 | yield redis 22 | 23 | 24 | def test_redis_stub(redis): 25 | # We don't yet have a test key set. 26 | assert redis.get("test") is None 27 | 28 | # Set the test key to abc. 29 | redis.set("test", "abc") 30 | assert redis.get("test").decode() == "abc" 31 | 32 | # Test expire. 33 | redis.expire("test", 0) 34 | assert redis.get("test") is None 35 | 36 | # Now, set the test key again and use delete() on it. 37 | redis.set("test", "abc") 38 | assert redis.get("test").decode() == "abc" 39 | redis.delete("test") 40 | assert redis.get("test") is None 41 | -------------------------------------------------------------------------------- /test/test_relation_type.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from aurweb import db 4 | from aurweb.models.relation_type import RelationType 5 | 6 | 7 | @pytest.fixture(autouse=True) 8 | def setup(db_test): 9 | return 10 | 11 | 12 | def test_relation_type_creation(): 13 | with db.begin(): 14 | relation_type = db.create(RelationType, Name="test-relation") 15 | 16 | assert bool(relation_type.ID) 17 | assert relation_type.Name == "test-relation" 18 | 19 | with db.begin(): 20 | db.delete(relation_type) 21 | 22 | 23 | def test_relation_types(): 24 | conflicts = db.query(RelationType, RelationType.Name == "conflicts").first() 25 | assert conflicts is not None 26 | assert conflicts.Name == "conflicts" 27 | 28 | provides = db.query(RelationType, RelationType.Name == "provides").first() 29 | assert provides is not None 30 | assert provides.Name == "provides" 31 | 32 | replaces = db.query(RelationType, RelationType.Name == "replaces").first() 33 | assert replaces is not None 34 | assert replaces.Name == "replaces" 35 | -------------------------------------------------------------------------------- /test/test_request_type.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from aurweb import db 4 | from aurweb.models.request_type import DELETION_ID, MERGE_ID, ORPHAN_ID, RequestType 5 | 6 | 7 | @pytest.fixture(autouse=True) 8 | def setup(db_test): 9 | return 10 | 11 | 12 | def test_request_type_creation(): 13 | with db.begin(): 14 | request_type = db.create(RequestType, Name="Test Request") 15 | 16 | assert bool(request_type.ID) 17 | assert request_type.Name == "Test Request" 18 | 19 | with db.begin(): 20 | db.delete(request_type) 21 | 22 | 23 | def test_request_type_null_name_returns_empty_string(): 24 | with db.begin(): 25 | request_type = db.create(RequestType) 26 | 27 | assert bool(request_type.ID) 28 | assert request_type.Name == str() 29 | 30 | with db.begin(): 31 | db.delete(request_type) 32 | 33 | 34 | def test_request_type_name_display(): 35 | deletion = db.query(RequestType, RequestType.ID == DELETION_ID).first() 36 | assert deletion.name_display() == "Deletion" 37 | 38 | orphan = db.query(RequestType, RequestType.ID == ORPHAN_ID).first() 39 | assert orphan.name_display() == "Orphan" 40 | 41 | merge = db.query(RequestType, RequestType.ID == MERGE_ID).first() 42 | assert merge.name_display() == "Merge" 43 | -------------------------------------------------------------------------------- /test/test_term.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from sqlalchemy.exc import IntegrityError 3 | 4 | from aurweb import db 5 | from aurweb.models.term import Term 6 | 7 | 8 | @pytest.fixture(autouse=True) 9 | def setup(db_test): 10 | return 11 | 12 | 13 | def test_term_creation(): 14 | with db.begin(): 15 | term = db.create( 16 | Term, Description="Term description", URL="https://fake_url.io" 17 | ) 18 | assert bool(term.ID) 19 | assert term.Description == "Term description" 20 | assert term.URL == "https://fake_url.io" 21 | assert term.Revision == 1 22 | 23 | 24 | def test_term_null_description_raises_exception(): 25 | with pytest.raises(IntegrityError): 26 | Term(URL="https://fake_url.io") 27 | 28 | 29 | def test_term_null_url_raises_exception(): 30 | with pytest.raises(IntegrityError): 31 | Term(Description="Term description") 32 | -------------------------------------------------------------------------------- /test/test_time.py: -------------------------------------------------------------------------------- 1 | import aurweb.config 2 | from aurweb.testing.requests import Request 3 | from aurweb.time import get_request_timezone, tz_offset 4 | 5 | 6 | def test_tz_offset_utc(): 7 | offset = tz_offset("UTC") 8 | assert offset == "+00:00" 9 | 10 | 11 | def test_tz_offset_mst(): 12 | offset = tz_offset("MST") 13 | assert offset == "-07:00" 14 | 15 | 16 | def test_request_timezone(): 17 | request = Request() 18 | 19 | # Default timezone 20 | dtz = aurweb.config.get("options", "default_timezone") 21 | assert get_request_timezone(request) == dtz 22 | 23 | # Timezone from query params 24 | request.query_params = {"timezone": "Europe/Berlin"} 25 | assert get_request_timezone(request) == "Europe/Berlin" 26 | 27 | # Timezone from authenticated user. 28 | request.query_params = {} 29 | request.user.authenticated = True 30 | request.user.Timezone = "America/Los_Angeles" 31 | assert get_request_timezone(request) == "America/Los_Angeles" 32 | 33 | # Timezone from authenticated user with query param 34 | # Query param should have precedence 35 | request.query_params = {"timezone": "Europe/Berlin"} 36 | assert get_request_timezone(request) == "Europe/Berlin" 37 | -------------------------------------------------------------------------------- /upgrading/1.2.10.txt: -------------------------------------------------------------------------------- 1 | ALTER TABLE Packages MODIFY Description CHAR(255) NOT NULL DEFAULT "An Arch Package"; 2 | -------------------------------------------------------------------------------- /upgrading/1.3.0.txt: -------------------------------------------------------------------------------- 1 | ALTER TABLE PackageDepends ADD COLUMN DepCondition VARCHAR(20) AFTER DepPkgID; 2 | ALTER TABLE Packages ADD License CHAR(40) NOT NULL DEFAULT ''; 3 | -------------------------------------------------------------------------------- /upgrading/1.5.2.txt: -------------------------------------------------------------------------------- 1 | 1. Ensure Pear and File/Find.php are in the path. See web/README.txt. 2 | 3 | 2. Update your running copy of support/scripts/newpackage-notify. 4 | 5 | 3. Run this in web/lib: 6 | "/packages/$1/$1$2" ) 32 | ---- 33 | 34 | If you use a non-standard URL_DIR, slight modifications might be necessary. 35 | 36 | 8. Merge "scripts/aurblup/config.h.proto" with "scripts/aurblup/config.h". 37 | -------------------------------------------------------------------------------- /upgrading/1.9.1.txt: -------------------------------------------------------------------------------- 1 | 1. Merge "web/lib/config.inc.php.proto" with "web/lib/config.inc.php". 2 | 3 | 2. Install translations by running `make install` in "po/". 4 | -------------------------------------------------------------------------------- /upgrading/2.0.0.txt: -------------------------------------------------------------------------------- 1 | 1. Add new "Users" table login date and PGP key columns: 2 | 3 | ---- 4 | ALTER TABLE Users ADD COLUMN LastLogin BIGINT NOT NULL DEFAULT 0; 5 | ALTER TABLE Users ADD COLUMN PGPKey VARCHAR(40) NULL DEFAULT NULL; 6 | ---- 7 | 8 | 2. Merge "web/lib/config.inc.php.proto" with "web/lib/config.inc.php". 9 | 10 | 3. Enable the PDO MySQL extension (pdo_mysql.so) in "php.ini". 11 | 12 | 4. Upgrade to PHP>=5.4.0 or enable "short_open_tag" in "php.ini". 13 | 14 | 5. Install translations by running `make install` in "po/". 15 | -------------------------------------------------------------------------------- /upgrading/2.1.0.txt: -------------------------------------------------------------------------------- 1 | 1. Update your aurblup setup to match configuration changes. See commit 2 | 6dc61e7d9e87ad6821869dab61e5f005af2e0252 for details. 3 | -------------------------------------------------------------------------------- /upgrading/2.2.0.txt: -------------------------------------------------------------------------------- 1 | 1. Add new "Users" table login IP address column: 2 | 3 | ---- 4 | ALTER TABLE Users 5 | ADD COLUMN LastLoginIPAddress INTEGER UNSIGNED NOT NULL DEFAULT 0; 6 | ---- 7 | 8 | 2. Add a new "Bans" table: 9 | 10 | ---- 11 | CREATE TABLE Bans ( 12 | IPAddress INTEGER UNSIGNED NOT NULL DEFAULT 0, 13 | BanTS TIMESTAMP NOT NULL, 14 | PRIMARY KEY (IPAddress) 15 | ) ENGINE = InnoDB; 16 | ---- 17 | -------------------------------------------------------------------------------- /upgrading/2.3.0.txt: -------------------------------------------------------------------------------- 1 | 1. Add registration and inactivity time stamps to the "Users" table: 2 | 3 | ---- 4 | ALTER TABLE Users 5 | ADD COLUMN RegistrationTS TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 6 | ADD COLUMN InactivityTS BIGINT NOT NULL DEFAULT 0; 7 | ---- 8 | 9 | 2. Add fields to store the total number of TUs and the quorum to the 10 | "TU_VoteInfo" table: 11 | 12 | ---- 13 | ALTER TABLE TU_VoteInfo 14 | ADD COLUMN ActiveTUs tinyint(3) unsigned NOT NULL default '0', 15 | ADD COLUMN Quorum decimal(2, 2) unsigned NOT NULL; 16 | ---- 17 | 18 | 3. Add a "fonts" category: 19 | 20 | ---- 21 | INSERT INTO PackageCategories (Category) VALUES ('fonts'); 22 | ---- 23 | -------------------------------------------------------------------------------- /upgrading/3.1.0.txt: -------------------------------------------------------------------------------- 1 | 1. Increase the size of all fields containing package names, license names, 2 | group names or package versions: 3 | 4 | ---- 5 | ALTER TABLE PackageBases MODIFY Name VARCHAR(255) NOT NULL; 6 | ALTER TABLE Packages 7 | MODIFY Name VARCHAR(255) NOT NULL, 8 | MODIFY Version VARCHAR(255) NOT NULL DEFAULT ''; 9 | ALTER TABLE Licenses MODIFY Name VARCHAR(255) NOT NULL; 10 | ALTER TABLE `Groups` MODIFY Name VARCHAR(255) NOT NULL; 11 | ALTER TABLE PackageDepends MODIFY DepCondition VARCHAR(255); 12 | ALTER TABLE PackageRelations MODIFY RelCondition VARCHAR(255); 13 | ---- 14 | -------------------------------------------------------------------------------- /upgrading/3.2.0.txt: -------------------------------------------------------------------------------- 1 | 1. Add support for package requests to the database: 2 | 3 | ---- 4 | CREATE TABLE RequestTypes ( 5 | ID TINYINT UNSIGNED NOT NULL AUTO_INCREMENT, 6 | Name VARCHAR(32) NOT NULL DEFAULT '', 7 | PRIMARY KEY (ID) 8 | ) ENGINE = InnoDB; 9 | INSERT INTO RequestTypes VALUES (1, 'deletion'); 10 | INSERT INTO RequestTypes VALUES (2, 'orphan'); 11 | INSERT INTO RequestTypes VALUES (3, 'merge'); 12 | 13 | CREATE TABLE PackageRequests ( 14 | ID BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, 15 | ReqTypeID TINYINT UNSIGNED NOT NULL, 16 | PackageBaseID INTEGER UNSIGNED NULL, 17 | PackageBaseName VARCHAR(255) NOT NULL, 18 | MergeBaseName VARCHAR(255) NULL, 19 | UsersID INTEGER UNSIGNED NULL DEFAULT NULL, 20 | Comments TEXT NOT NULL DEFAULT '', 21 | RequestTS BIGINT UNSIGNED NOT NULL DEFAULT 0, 22 | Status TINYINT UNSIGNED NOT NULL DEFAULT 0, 23 | PRIMARY KEY (ID), 24 | INDEX (UsersID), 25 | INDEX (PackageBaseID), 26 | FOREIGN KEY (ReqTypeID) REFERENCES RequestTypes(ID) ON DELETE NO ACTION, 27 | FOREIGN KEY (UsersID) REFERENCES Users(ID) ON DELETE SET NULL, 28 | FOREIGN KEY (PackageBaseID) REFERENCES PackageBases(ID) ON DELETE SET NULL 29 | ) ENGINE = InnoDB; 30 | ---- 31 | -------------------------------------------------------------------------------- /upgrading/3.4.0.txt: -------------------------------------------------------------------------------- 1 | 1. Add the "Package Maintainer & Developer" user group: 2 | 3 | ---- 4 | INSERT INTO AccountTypes (ID, AccountType) VALUES (4, 'Package Maintainer & Developer'); 5 | ---- 6 | -------------------------------------------------------------------------------- /upgrading/3.5.0.txt: -------------------------------------------------------------------------------- 1 | 1. Add support for architecture-specific dependencies to the database: 2 | 3 | ---- 4 | ALTER TABLE PackageDepends ADD COLUMN DepArch VARCHAR(255) NULL DEFAULT NULL; 5 | ALTER TABLE PackageRelations ADD COLUMN RelArch VARCHAR(255) NULL DEFAULT NULL; 6 | ALTER TABLE PackageSources ADD COLUMN SourceArch VARCHAR(255) NULL DEFAULT NULL; 7 | ---- 8 | 9 | 2. Add a time stamp column to the package votes table: 10 | 11 | ---- 12 | ALTER TABLE PackageVotes ADD COLUMN VoteTS BIGINT NULL DEFAULT NULL; 13 | ---- 14 | 15 | 3. Add a "wayland" category: 16 | 17 | ---- 18 | INSERT INTO PackageCategories (Category) VALUES ('wayland'); 19 | ---- 20 | 21 | 4. The configuration file format has been changed. Make sure you convert 22 | web/lib/config.inc.php to the new format (see conf/config.proto for an example 23 | configuration) and put the resulting file in conf/config. 24 | 25 | 5. Support for non-virtual URLs has been removed. 26 | -------------------------------------------------------------------------------- /upgrading/4.1.0.txt: -------------------------------------------------------------------------------- 1 | 1. Add a timestamp for comment editing/deletion and an ID of the last user 2 | who edited a comment: 3 | 4 | ---- 5 | ALTER TABLE PackageComments 6 | ADD COLUMN EditedTS BIGINT UNSIGNED NULL DEFAULT NULL, 7 | ADD COLUMN EditedUsersID INTEGER UNSIGNED NULL DEFAULT NULL, 8 | ADD FOREIGN KEY (EditedUsersID) REFERENCES Users(ID) ON DELETE SET NULL; 9 | ---- 10 | 11 | 2. Add fields to store the ID and comment of the last user who flagged a 12 | package out-of-date: 13 | 14 | ---- 15 | ALTER TABLE PackageBases 16 | ADD COLUMN FlaggerUID INTEGER UNSIGNED NULL DEFAULT NULL, 17 | ADD COLUMN FlaggerComment VARCHAR(255) NOT NULL, 18 | ADD FOREIGN KEY (FlaggerUID) REFERENCES Users(ID) ON DELETE SET NULL; 19 | ---- 20 | 21 | 3. Add field to store the state of a user's email address: 22 | 23 | ---- 24 | ALTER TABLE Users 25 | ADD COLUMN HideEmail TINYINT UNSIGNED NOT NULL DEFAULT 0; 26 | ---- 27 | -------------------------------------------------------------------------------- /upgrading/4.2.0.txt: -------------------------------------------------------------------------------- 1 | 1. Add a new table to store providers from official packages: 2 | 3 | ---- 4 | CREATE TABLE OfficialProviders ( 5 | ID INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, 6 | Name VARCHAR(64) NOT NULL, 7 | Provides VARCHAR(64) NOT NULL, 8 | PRIMARY KEY (ID) 9 | ) ENGINE = InnoDB; 10 | CREATE UNIQUE INDEX ProviderNameProvides ON OfficialProviders (Name, Provides); 11 | ---- 12 | 13 | 2. Resize the email address field: 14 | 15 | ---- 16 | ALTER TABLE Users MODIFY Email VARCHAR(254) NOT NULL; 17 | ---- 18 | 19 | 3. Add new columns to the PackageComments table: 20 | 21 | ---- 22 | ALTER TABLE PackageComments 23 | ADD COLUMN DelTS BIGINT UNSIGNED NULL DEFAULT NULL, 24 | ADD COLUMN PinnedTS BIGINT UNSIGNED NOT NULL DEFAULT 0; 25 | ---- 26 | 27 | 4. Update the deletion time stamp of all deleted comments: 28 | 29 | ---- 30 | UPDATE PackageComments SET DelTS = EditedTS WHERE DelUsersID IS NOT NULL; 31 | ---- 32 | 33 | 5. Add new column to store the closure comment of package requests: 34 | 35 | ---- 36 | ALTER TABLE PackageRequests ADD COLUMN ClosureComment TEXT NOT NULL DEFAULT ''; 37 | ---- 38 | 39 | 6. Change FlaggerComment from VARCHAR to TEXT: 40 | 41 | ---- 42 | ALTER TABLE PackageBases MODIFY COLUMN FlaggerComment TEXT NOT NULL DEFAULT ''; 43 | ---- 44 | 45 | 7. Rename the CommentNotify table to PackageNotifications: 46 | 47 | ---- 48 | ALTER TABLE CommentNotify RENAME TO PackageNotifications; 49 | ---- 50 | 51 | 8. Add new columns to store notification settings: 52 | 53 | ---- 54 | ALTER TABLE Users 55 | ADD COLUMN CommentNotify TINYINT(1) NOT NULL DEFAULT 1, 56 | ADD COLUMN UpdateNotify TINYINT(1) NOT NULL DEFAULT 0; 57 | ---- 58 | -------------------------------------------------------------------------------- /upgrading/4.2.1.txt: -------------------------------------------------------------------------------- 1 | 1. Convert the LastLoginIPAddress column to VARCHAR(40): 2 | 3 | ---- 4 | ALTER TABLE Users MODIFY LastLoginIPAddress VARCHAR(40) NULL DEFAULT NULL; 5 | ---- 6 | -------------------------------------------------------------------------------- /upgrading/4.3.0.txt: -------------------------------------------------------------------------------- 1 | 1. Add a column to store ownership notification settings: 2 | 3 | ---- 4 | ALTER TABLE Users ADD COLUMN OwnershipNotify TINYINT(1) NOT NULL DEFAULT 1; 5 | ---- 6 | 7 | 2. Resize the LastLoginIPAddress column: 8 | 9 | ---- 10 | ALTER TABLE Users MODIFY LastLoginIPAddress VARCHAR(45) NULL DEFAULT NULL; 11 | ---- 12 | 13 | 3. Add a new column to store repository information of official providers: 14 | 15 | ---- 16 | ALTER TABLE OfficialProviders ADD COLUMN Repo VARCHAR(64) NOT NULL; 17 | ---- 18 | 19 | 4. Add a column to store users' homepages: 20 | 21 | ---- 22 | ALTER TABLE Users ADD COLUMN Homepage TEXT NULL DEFAULT NULL; 23 | ---- 24 | 25 | 5. Resize LangPreference to fit Latin American Spanish language code: 26 | 27 | -- 28 | ALTER TABLE Users MODIFY LangPreference VARCHAR(6); 29 | -- 30 | -------------------------------------------------------------------------------- /upgrading/4.4.0.txt: -------------------------------------------------------------------------------- 1 | 1. Resize the URL column of the Packages table: 2 | 3 | ---- 4 | ALTER TABLE Packages MODIFY URL VARCHAR(8000) NULL DEFAULT NULL; 5 | ---- 6 | 7 | 2. Resize the Source column of the PackageSources table: 8 | 9 | ---- 10 | ALTER TABLE PackageSources 11 | MODIFY Source VARCHAR(8000) NOT NULL DEFAULT "/dev/null"; 12 | ---- 13 | 14 | 3. The location of the Git interface scripts was changed. Make sure you update 15 | your aurweb configuration, as well as the SSH daemon and AUR Git repository 16 | configurations to point to the new wrapper scripts which are located in 17 | /usr/local/bin/ by default. 18 | -------------------------------------------------------------------------------- /upgrading/4.4.1.txt: -------------------------------------------------------------------------------- 1 | 1. The default configuration file search path now points to /etc/aurweb/config. 2 | Make sure you copy your aurweb configuration to the new location before 3 | upgrading. 4 | 5 | 2. The maintenance scripts have been prefixed by "aurweb-" and can now be 6 | installed using `python3 setup.py install`. 7 | -------------------------------------------------------------------------------- /upgrading/4.5.0.txt: -------------------------------------------------------------------------------- 1 | 1. Add Timezone column to Users: 2 | 3 | --- 4 | ALTER TABLE Users ADD COLUMN Timezone VARCHAR(32) NOT NULL DEFAULT 'UTC'; 5 | --- 6 | 7 | 2. Add LastSSHLogin and LastSSHLoginIPAddress columns to the Users table: 8 | 9 | --- 10 | ALTER TABLE Users 11 | ADD COLUMN LastSSHLogin BIGINT UNSIGNED NOT NULL DEFAULT 0, 12 | ADD COLUMN LastSSHLoginIPAddress VARCHAR(45) NULL DEFAULT NULL; 13 | --- 14 | 15 | 3. Convert the IPAddress column of the Bans table to VARCHAR(45). If the table 16 | contains any active bans, convert them accordingly: 17 | 18 | ---- 19 | ALTER TABLE Bans MODIFY IPAddress VARCHAR(45) NULL DEFAULT NULL; 20 | ---- 21 | 22 | 4. Resize the Passwd column of the Users table: 23 | 24 | --- 25 | ALTER TABLE Users MODIFY Passwd VARCHAR(255) NOT NULL; 26 | --- 27 | -------------------------------------------------------------------------------- /upgrading/4.6.0.txt: -------------------------------------------------------------------------------- 1 | 1. Add DepDesc column to PackageDepends and split dependency names: 2 | 3 | --- 4 | ALTER TABLE PackageDepends ADD COLUMN DepDesc VARCHAR(255) NULL DEFAULT NULL; 5 | UPDATE PackageDepends 6 | SET DepDesc = SUBSTRING(DepName FROM POSITION(': ' IN DepName) + 2) 7 | WHERE POSITION(': ' IN DepName) > 0; 8 | UPDATE PackageDepends 9 | SET DepName = SUBSTRING(DepName FROM 1 FOR POSITION(': ' IN DepName) - 1) 10 | WHERE POSITION(': ' IN DepName) > 0; 11 | --- 12 | 13 | 2. Add RenderedComment column to PackageComments: 14 | 15 | --- 16 | ALTER TABLE PackageComments ADD COLUMN RenderedComment TEXT NOT NULL; 17 | --- 18 | 19 | 3. Add Terms and AcceptedTerms tables: 20 | 21 | --- 22 | CREATE TABLE Terms ( 23 | ID INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, 24 | Description VARCHAR(255) NOT NULL, 25 | URL VARCHAR(8000) NOT NULL, 26 | Revision INTEGER UNSIGNED NOT NULL DEFAULT 1, 27 | PRIMARY KEY (ID) 28 | ) ENGINE = InnoDB; 29 | 30 | CREATE TABLE AcceptedTerms ( 31 | UsersID INTEGER UNSIGNED NOT NULL, 32 | TermsID INTEGER UNSIGNED NOT NULL, 33 | Revision INTEGER UNSIGNED NOT NULL DEFAULT 0, 34 | FOREIGN KEY (UsersID) REFERENCES Users(ID) ON DELETE CASCADE, 35 | FOREIGN KEY (TermsID) REFERENCES Terms(ID) ON DELETE CASCADE 36 | ) ENGINE = InnoDB; 37 | --- 38 | -------------------------------------------------------------------------------- /upgrading/4.7.0.txt: -------------------------------------------------------------------------------- 1 | 1. Add ApiRateLimit table: 2 | 3 | --- 4 | CREATE TABLE `ApiRateLimit` ( 5 | IP VARCHAR(45) NOT NULL, 6 | Requests INT(11) NOT NULL, 7 | WindowStart BIGINT(20) NOT NULL, 8 | PRIMARY KEY (`ip`) 9 | ) ENGINE = InnoDB; 10 | CREATE INDEX ApiRateLimitWindowStart ON ApiRateLimit (WindowStart); 11 | --- 12 | -------------------------------------------------------------------------------- /upgrading/5.x.x.txt: -------------------------------------------------------------------------------- 1 | Starting from release 5.0.0, Alembic is used for managing database migrations. 2 | 3 | Run `alembic upgrade head` from the aurweb root directory to upgrade your 4 | database after upgrading the source code to a new release. 5 | 6 | When upgrading from 4.8.0, you also need to execute the following manual SQL 7 | statements before doing so. 8 | 9 | 1. Add new columns to store the timestamp and UID when closing requests: 10 | 11 | ---- 12 | ALTER TABLE PackageRequests ADD COLUMN ClosedTS BIGINT UNSIGNED NULL DEFAULT NULL; 13 | ALTER TABLE PackageRequests ADD COLUMN ClosedUID INTEGER UNSIGNED NULL DEFAULT NULL; 14 | ---- 15 | 16 | 2. Add a new column to store backup email addresses: 17 | 18 | ---- 19 | ALTER TABLE Users ADD COLUMN BackupEmail VARCHAR(254) NULL DEFAULT NULL; 20 | ---- 21 | -------------------------------------------------------------------------------- /upgrading/longerpkgname.txt: -------------------------------------------------------------------------------- 1 | ALTER TABLE Packages MODIFY Name CHAR(64) NOT NULL; 2 | -------------------------------------------------------------------------------- /upgrading/post-checkout: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ( cd doc/ && make ) 4 | ( cd po/ && make && make install ) 5 | -------------------------------------------------------------------------------- /util/sendmail: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Send email to temporary filesystem for tests. 3 | dir='test-emails' 4 | filename='email.txt' 5 | if [ ! -z ${PYTEST_CURRENT_TEST+x} ]; then 6 | filename="$(echo $PYTEST_CURRENT_TEST | cut -d ' ' -f 1 | sed -r 's/(\/|\.|,|:)/_/g')" 7 | fi 8 | mkdir -p "$dir" 9 | 10 | path="${dir}/${filename}" 11 | serial_file="${path}.serial" 12 | if [ ! -f $serial_file ]; then 13 | echo 0 > $serial_file 14 | fi 15 | 16 | # Increment and update $serial_file. 17 | serial=$(($(cat $serial_file) + 1)) 18 | echo $serial > $serial_file 19 | 20 | # Use the serial we're on to mark the email file. 21 | # Emails have the format: PYTEST_CURRENT_TEST.s.txt 22 | # where s is the current serial for PYTEST_CURRENT_TEST. 23 | cat > "${path}.${serial}.txt" 24 | 25 | exit 0 26 | --------------------------------------------------------------------------------