wtf
This commit is contained in:
parent
6865ce91fa
commit
27c9f79dd5
19
debian/rules
vendored
19
debian/rules
vendored
@ -1,14 +1,16 @@
|
|||||||
#!/usr/bin/make -f
|
#!/usr/bin/make -f
|
||||||
|
|
||||||
|
### WTF ###
|
||||||
# See debhelper(7) (uncomment to enable)
|
# See debhelper(7) (uncomment to enable)
|
||||||
# output every command that modifies files on the build system.
|
# output every command that modifies files on the build system.
|
||||||
#export DH_VERBOSE = 1
|
#export DH_VERBOSE = 1
|
||||||
|
#
|
||||||
export PYBUILD_NAME=repolib
|
#export PYBUILD_NAME=repolib
|
||||||
export PYBUILD_OPTION=--test-pytest
|
#export PYBUILD_OPTION=--test-pytest
|
||||||
|
#
|
||||||
%:
|
#%:
|
||||||
dh $@ --with python3 --buildsystem=pybuild
|
# dh $@ --with python3 --buildsystem=pybuild
|
||||||
|
#
|
||||||
## Uncomment to disable testing during package builds
|
## Uncomment to disable testing during package builds
|
||||||
## NOTE for QA or Engineering Review: This should not be uncommented in a
|
## NOTE for QA or Engineering Review: This should not be uncommented in a
|
||||||
## PR. If it is, DO NOT APPROVE THE PR!!!
|
## PR. If it is, DO NOT APPROVE THE PR!!!
|
||||||
@ -21,4 +23,7 @@ export PYBUILD_OPTION=--test-pytest
|
|||||||
# dh_auto_build
|
# dh_auto_build
|
||||||
# PYTHONPATH=. http_proxy='127.0.0.1:9' sphinx-build -N -bhtml docs/ build/html # HTML generator
|
# PYTHONPATH=. http_proxy='127.0.0.1:9' sphinx-build -N -bhtml docs/ build/html # HTML generator
|
||||||
# PYTHONPATH=. http_proxy='127.0.0.1:9' sphinx-build -N -bman docs/ build/man # Manpage generator
|
# PYTHONPATH=. http_proxy='127.0.0.1:9' sphinx-build -N -bman docs/ build/man # Manpage generator
|
||||||
|
### WTF ###
|
||||||
|
|
||||||
|
%:
|
||||||
|
dh $@
|
||||||
|
11
main.sh
11
main.sh
@ -4,16 +4,19 @@ add-apt-repository https://ppa.pika-os.com
|
|||||||
add-apt-repository ppa:pikaos/pika
|
add-apt-repository ppa:pikaos/pika
|
||||||
add-apt-repository ppa:kubuntu-ppa/backports
|
add-apt-repository ppa:kubuntu-ppa/backports
|
||||||
# Clone Upstream
|
# Clone Upstream
|
||||||
git clone https://github.com/pop-os/repolib
|
### WTF ###
|
||||||
rm -rvf ./repolib/debian
|
#git clone https://github.com/pop-os/repolib
|
||||||
cp -rvf ./debian ./repolib
|
#rm -rvf ./repolib/debian
|
||||||
|
### WTF ###
|
||||||
|
cp -rvf ./python3-repolib.install ./debian/
|
||||||
|
cp -rvf ./debian ./repolib/
|
||||||
cd ./repolib
|
cd ./repolib
|
||||||
|
|
||||||
# Get build deps
|
# Get build deps
|
||||||
apt-get build-dep ./ -y
|
apt-get build-dep ./ -y
|
||||||
|
|
||||||
# Build package
|
# Build package
|
||||||
dpkg-buildpackage
|
dpkg-buildpackage --no-sign
|
||||||
|
|
||||||
# Move the debs to output
|
# Move the debs to output
|
||||||
cd ../
|
cd ../
|
||||||
|
2
python3-repolib.install
Normal file
2
python3-repolib.install
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
usr
|
||||||
|
etc
|
20
repolib/etc/dbus-1/system.d/org.pop_os.repolib.conf
Normal file
20
repolib/etc/dbus-1/system.d/org.pop_os.repolib.conf
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
|
||||||
|
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
|
||||||
|
<busconfig>
|
||||||
|
<type>system</type>
|
||||||
|
<!-- Only root can own the service -->
|
||||||
|
<policy user="root">
|
||||||
|
<allow own="org.pop_os.repolib"/>
|
||||||
|
<allow send_destination="org.pop_os.repolib"/>
|
||||||
|
<allow receive_sender="org.pop_os.repolib"/>
|
||||||
|
</policy>
|
||||||
|
<policy group="adm">
|
||||||
|
<allow send_destination="org.pop_os.repolib"/>
|
||||||
|
<allow receive_sender="org.pop_os.repolib"/>
|
||||||
|
</policy>
|
||||||
|
<policy group="sudo">
|
||||||
|
<allow send_destination="org.pop_os.repolib"/>
|
||||||
|
<allow receive_sender="org.pop_os.repolib"/>
|
||||||
|
</policy>
|
||||||
|
</busconfig>
|
85
repolib/usr/bin/apt-manage
Executable file
85
repolib/usr/bin/apt-manage
Executable file
@ -0,0 +1,85 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
"""
|
||||||
|
Copyright (c) 2019-2020, Ian Santopietro
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
This file is part of RepoLib.
|
||||||
|
|
||||||
|
RepoLib is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
RepoLib is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public License
|
||||||
|
along with RepoLib. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
"""
|
||||||
|
#pylint: disable=invalid-name
|
||||||
|
# Pylint will complain about our module name not being snake_case, however this
|
||||||
|
# is a command rather than a python module, and thus this is correct anyway.
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import repolib
|
||||||
|
from repolib import command
|
||||||
|
|
||||||
|
SOURCES_DIR = '/etc/apt/sources.list.d'
|
||||||
|
|
||||||
|
def main(options=None):
|
||||||
|
""" Main function for apt-manage."""
|
||||||
|
# Set up Argument Parsing.
|
||||||
|
parser = repolib.command.parser
|
||||||
|
|
||||||
|
# Parse options
|
||||||
|
args = parser.parse_args()
|
||||||
|
if options:
|
||||||
|
args = parser.parse_args(options)
|
||||||
|
|
||||||
|
if not args.debug:
|
||||||
|
args.debug = 0
|
||||||
|
|
||||||
|
if args.debug > 2:
|
||||||
|
args.debug = 2
|
||||||
|
|
||||||
|
verbosity = {
|
||||||
|
0 : logging.WARN,
|
||||||
|
1 : logging.INFO,
|
||||||
|
2 : logging.DEBUG
|
||||||
|
}
|
||||||
|
|
||||||
|
log = logging.getLogger('apt-manage')
|
||||||
|
handler = logging.StreamHandler()
|
||||||
|
formatter = logging.Formatter('%(name)s: %(levelname)s: %(message)s')
|
||||||
|
handler.setFormatter(formatter)
|
||||||
|
log.addHandler(handler)
|
||||||
|
log.setLevel(verbosity[args.debug])
|
||||||
|
log.debug('Logging set up!')
|
||||||
|
repolib.set_logging_level(args.debug)
|
||||||
|
|
||||||
|
if not args.action:
|
||||||
|
args = parser.parse_args(sys.argv[1:] + ['list'])
|
||||||
|
|
||||||
|
log.debug('Arguments passed: %s', str(args))
|
||||||
|
log.debug('Got command: %s', args.action)
|
||||||
|
|
||||||
|
subcommand = args.action.capitalize()
|
||||||
|
|
||||||
|
command = getattr(repolib.command, subcommand)(log, args, parser)
|
||||||
|
result = command.run()
|
||||||
|
if not result:
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print('')
|
||||||
|
sys.exit(130)
|
@ -0,0 +1,182 @@
|
|||||||
|
Metadata-Version: 2.1
|
||||||
|
Name: repolib
|
||||||
|
Version: 2.2.0
|
||||||
|
Summary: Easily manage software sources
|
||||||
|
Home-page: https://github.com/pop-os/repolib
|
||||||
|
Author: Ian Santopietro
|
||||||
|
Author-email: ian@system76.com
|
||||||
|
License: LGPLv3
|
||||||
|
Download-URL: https://github.com/pop-os/repolib/releases
|
||||||
|
Platform: UNKNOWN
|
||||||
|
License-File: LICENSE
|
||||||
|
License-File: LICENSE.LESSER
|
||||||
|
|
||||||
|
=======
|
||||||
|
RepoLib
|
||||||
|
=======
|
||||||
|
|
||||||
|
RepoLib is a Python library and CLI tool-set for managing your software
|
||||||
|
system software repositories. It's currently set up to handle APT repositories
|
||||||
|
on Debian-based linux distributions.
|
||||||
|
|
||||||
|
RepoLib is intended to operate on DEB822-format sources. It aims to provide
|
||||||
|
feature parity with software-properties for most commonly used functions.
|
||||||
|
|
||||||
|
Documentation
|
||||||
|
=============
|
||||||
|
|
||||||
|
Documentation is available online at `Read The Docs <https://repolib.rtfd.io/>`_.
|
||||||
|
|
||||||
|
|
||||||
|
Basic CLI Usage
|
||||||
|
---------------
|
||||||
|
|
||||||
|
RepoLib includes a CLI program for managing software repositories,
|
||||||
|
:code:`apt-manage`
|
||||||
|
.
|
||||||
|
|
||||||
|
Usage is divided into subcommands for most tasks. Currently implemented commands
|
||||||
|
are:
|
||||||
|
|
||||||
|
apt-manage add # Adds repositories to the system
|
||||||
|
apt-manage list # Lists configuration details of repositories
|
||||||
|
|
||||||
|
Additional information is available with the built-in help:
|
||||||
|
|
||||||
|
apt-manage --help
|
||||||
|
|
||||||
|
|
||||||
|
Add
|
||||||
|
^^^
|
||||||
|
|
||||||
|
Apt-manage allows entering a URL for a repository, a complete debian line, or a
|
||||||
|
Launchpad PPA shortcut (e.g. "ppa:user/repo"). It also adds signing keys for PPA
|
||||||
|
style repositories automatically.
|
||||||
|
|
||||||
|
|
||||||
|
List
|
||||||
|
^^^^
|
||||||
|
|
||||||
|
With no options, it outputs a list of the currently configured repositories on
|
||||||
|
the system (all those found in
|
||||||
|
:code:`/etc/apt/sources.list.d/`
|
||||||
|
. With a configured repository as an argument, it outputs the configuration
|
||||||
|
details of the specified repository.
|
||||||
|
|
||||||
|
Remove
|
||||||
|
^^^^^^
|
||||||
|
|
||||||
|
Accepts one repository as an argument. Removes the specified repository.
|
||||||
|
|
||||||
|
NOTE: The system repository (/etc/at/sources.list.d/system.sources) cannot be
|
||||||
|
removed.
|
||||||
|
|
||||||
|
Source
|
||||||
|
^^^^^^
|
||||||
|
|
||||||
|
Allows enabling or disabling source code for the given repository.
|
||||||
|
|
||||||
|
Modify
|
||||||
|
^^^^^^
|
||||||
|
|
||||||
|
Allows changing configuration details of a given repository
|
||||||
|
|
||||||
|
Installation
|
||||||
|
============
|
||||||
|
|
||||||
|
From System Package Manager
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
If your operating system packages repolib, you can install it by running::
|
||||||
|
|
||||||
|
sudo apt install python3-repolib
|
||||||
|
|
||||||
|
|
||||||
|
Uninstall
|
||||||
|
^^^^^^^^^
|
||||||
|
|
||||||
|
To uninstall, simply do::
|
||||||
|
|
||||||
|
sudo apt remove python3-repolib
|
||||||
|
|
||||||
|
|
||||||
|
From PyPI
|
||||||
|
---------
|
||||||
|
|
||||||
|
Repolib is available on PyPI. You can install it for your current user with::
|
||||||
|
|
||||||
|
pip3 install repolib
|
||||||
|
|
||||||
|
Alternatively, you can install it system-wide using::
|
||||||
|
|
||||||
|
sudo pip3 install repolib
|
||||||
|
|
||||||
|
Uninstall
|
||||||
|
^^^^^^^^^
|
||||||
|
|
||||||
|
To uninstall, simply do::
|
||||||
|
|
||||||
|
sudo pip3 uninstall repolib
|
||||||
|
|
||||||
|
From Git
|
||||||
|
--------
|
||||||
|
|
||||||
|
First, clone the git repository onto your local system::
|
||||||
|
|
||||||
|
git clone https://github.com/isantop/repolib
|
||||||
|
cd repolib
|
||||||
|
|
||||||
|
Debian
|
||||||
|
------
|
||||||
|
|
||||||
|
On debian based distributions, you can build a .deb package locally and install
|
||||||
|
it onto your system. You will need the following build-dependencies:
|
||||||
|
|
||||||
|
* debhelper (>=11)
|
||||||
|
* dh-python
|
||||||
|
* python3-all
|
||||||
|
* python3-setuptools
|
||||||
|
|
||||||
|
You can use this command to install these all in one go::
|
||||||
|
|
||||||
|
sudo apt install debhelper dh-python python3-all python3-setuptools
|
||||||
|
|
||||||
|
Then build and install the package::
|
||||||
|
|
||||||
|
debuild -us -uc
|
||||||
|
cd ..
|
||||||
|
sudo dpkg -i python3-repolib_*.deb
|
||||||
|
|
||||||
|
Uninstall
|
||||||
|
^^^^^^^^^
|
||||||
|
|
||||||
|
To uninstall, simply do::
|
||||||
|
|
||||||
|
sudo apt remove python3-repolib
|
||||||
|
|
||||||
|
setuptools setup.py
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
You can build and install the package using python3-setuptools. First, install
|
||||||
|
the dependencies::
|
||||||
|
|
||||||
|
sudo apt install python3-all python3-setuptools
|
||||||
|
|
||||||
|
Then build and install the package::
|
||||||
|
|
||||||
|
sudo python3 ./setup.py install
|
||||||
|
|
||||||
|
Uninstall
|
||||||
|
^^^^^^^^^
|
||||||
|
|
||||||
|
You can uninstall RepoLib by removing the following files/directories:
|
||||||
|
|
||||||
|
* /usr/local/lib/python3.7/dist-packages/repolib/
|
||||||
|
* /usr/local/lib/python3.7/dist-packages/repolib-\*.egg-info
|
||||||
|
* /usr/local/bin/apt-manage
|
||||||
|
|
||||||
|
This command will remove all of these for you::
|
||||||
|
|
||||||
|
sudo rm -r /usr/local/lib/python3.7/dist-packages/repolib* /usr/local/bin/apt-manage
|
||||||
|
|
||||||
|
|
@ -0,0 +1 @@
|
|||||||
|
|
@ -0,0 +1 @@
|
|||||||
|
gnupg
|
@ -0,0 +1,4 @@
|
|||||||
|
repolib
|
||||||
|
repolib/command
|
||||||
|
repolib/shortcuts
|
||||||
|
repolib/unittest
|
144
repolib/usr/lib/python3/dist-packages/repolib/__init__.py
Normal file
144
repolib/usr/lib/python3/dist-packages/repolib/__init__.py
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
"""
|
||||||
|
Copyright (c) 2019-2022, Ian Santopietro
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
This file is part of RepoLib.
|
||||||
|
|
||||||
|
RepoLib is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
RepoLib is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public License
|
||||||
|
along with RepoLib. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import logging.handlers as handlers
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from . import __version__
|
||||||
|
|
||||||
|
VERSION = __version__.__version__
|
||||||
|
|
||||||
|
from .file import SourceFile, SourceFileError
|
||||||
|
from .source import Source, SourceError
|
||||||
|
from .shortcuts import PPASource, PopdevSource, shortcut_prefixes
|
||||||
|
from .key import SourceKey, KeyFileError
|
||||||
|
from . import util
|
||||||
|
from . import system
|
||||||
|
|
||||||
|
LOG_FILE_PATH = '/var/log/repolib.log'
|
||||||
|
LOG_LEVEL = logging.WARNING
|
||||||
|
KEYS_DIR = util.KEYS_DIR
|
||||||
|
SOURCES_DIR = util.SOURCES_DIR
|
||||||
|
TESTING = util.TESTING
|
||||||
|
KEYSERVER_QUERY_URL = util.KEYSERVER_QUERY_URL
|
||||||
|
DISTRO_CODENAME = util.DISTRO_CODENAME
|
||||||
|
PRETTY_PRINT = util.PRETTY_PRINT
|
||||||
|
CLEAN_CHARS = util.CLEAN_CHARS
|
||||||
|
|
||||||
|
try:
|
||||||
|
from systemd.journal import JournalHandler
|
||||||
|
systemd_support = True
|
||||||
|
except ImportError:
|
||||||
|
systemd_support = False
|
||||||
|
|
||||||
|
## Setup logging
|
||||||
|
stream_fmt = logging.Formatter(
|
||||||
|
'%(name)-21s: %(levelname)-8s %(message)s'
|
||||||
|
)
|
||||||
|
file_fmt = logging.Formatter(
|
||||||
|
'%(asctime)s - %(name)-21s: %(levelname)-8s %(message)s'
|
||||||
|
)
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
console_log = logging.StreamHandler()
|
||||||
|
console_log.setFormatter(stream_fmt)
|
||||||
|
console_log.setLevel(LOG_LEVEL)
|
||||||
|
|
||||||
|
# file_log = handlers.RotatingFileHandler(
|
||||||
|
# LOG_FILE_PATH, maxBytes=(1048576*5), backupCount=5
|
||||||
|
# )
|
||||||
|
# file_log.setFormatter(file_fmt)
|
||||||
|
# file_log.setLevel(LOG_LEVEL)
|
||||||
|
|
||||||
|
log.addHandler(console_log)
|
||||||
|
# log.addHandler(file_log)
|
||||||
|
|
||||||
|
log_level_map:dict = {
|
||||||
|
0: logging.WARNING,
|
||||||
|
1: logging.INFO,
|
||||||
|
2: logging.DEBUG
|
||||||
|
}
|
||||||
|
|
||||||
|
if systemd_support:
|
||||||
|
journald_log = JournalHandler() # type: ignore (this is handled by the wrapping if)
|
||||||
|
journald_log.setLevel(logging.INFO)
|
||||||
|
journald_log.setFormatter(stream_fmt)
|
||||||
|
log.addHandler(journald_log)
|
||||||
|
|
||||||
|
log.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
def set_testing(testing:bool = True) -> None:
|
||||||
|
"""Puts Repolib into testing mode"""
|
||||||
|
global KEYS_DIR
|
||||||
|
global SOURCES_DIR
|
||||||
|
|
||||||
|
util.set_testing(testing=testing)
|
||||||
|
KEYS_DIR = util.KEYS_DIR
|
||||||
|
SOURCES_DIR = util.SOURCES_DIR
|
||||||
|
|
||||||
|
|
||||||
|
def set_logging_level(level:int) -> None:
|
||||||
|
"""Set the logging level for this current repolib
|
||||||
|
|
||||||
|
Accepts an integer between 0 and 2, with 0 being the default loglevel of
|
||||||
|
logging.WARNING, 1 being logging.INFO, and 2 being logging.DEBUG.
|
||||||
|
|
||||||
|
Values greater than 2 are clamped to 2. Values less than 0 are clamped to 0.
|
||||||
|
|
||||||
|
Note: This only affects console output. Log file output remains
|
||||||
|
at logging.INFO
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
level(int): A logging level from 0-2
|
||||||
|
"""
|
||||||
|
if level > 2:
|
||||||
|
level = 2
|
||||||
|
if level < 0:
|
||||||
|
level = 0
|
||||||
|
LOG_LEVEL = log_level_map[level]
|
||||||
|
console_log.setLevel(LOG_LEVEL)
|
||||||
|
|
||||||
|
RepoError = util.RepoError
|
||||||
|
SourceFormat = util.SourceFormat
|
||||||
|
SourceType = util.SourceType
|
||||||
|
AptSourceEnabled = util.AptSourceEnabled
|
||||||
|
|
||||||
|
scrub_filename = util.scrub_filename
|
||||||
|
url_validator = util.url_validator
|
||||||
|
prettyprint_enable = util.prettyprint_enable
|
||||||
|
validate_debline = util.validate_debline
|
||||||
|
strip_hashes = util.strip_hashes
|
||||||
|
compare_sources = util.compare_sources
|
||||||
|
combine_sources = util.combine_sources
|
||||||
|
sources = util.sources
|
||||||
|
files = util.files
|
||||||
|
keys = util.keys
|
||||||
|
errors = util.errors
|
||||||
|
|
||||||
|
valid_keys = util.valid_keys
|
||||||
|
options_inmap = util.options_inmap
|
||||||
|
options_outmap = util.options_outmap
|
||||||
|
true_values = util.true_values
|
||||||
|
|
||||||
|
load_all_sources = system.load_all_sources
|
22
repolib/usr/lib/python3/dist-packages/repolib/__version__.py
Normal file
22
repolib/usr/lib/python3/dist-packages/repolib/__version__.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
"""
|
||||||
|
Copyright (c) 2019-2022, Ian Santopietro
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
This file is part of RepoLib.
|
||||||
|
|
||||||
|
RepoLib is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
RepoLib is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public License
|
||||||
|
along with RepoLib. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
"""
|
||||||
|
__version__ = "2.2.0"
|
@ -0,0 +1,30 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
"""
|
||||||
|
Copyright (c) 2022, Ian Santopietro
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
This file is part of RepoLib.
|
||||||
|
|
||||||
|
RepoLib is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
RepoLib is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public License
|
||||||
|
along with RepoLib. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .argparser import get_argparser
|
||||||
|
from .add import Add
|
||||||
|
from .list import List
|
||||||
|
from .remove import Remove
|
||||||
|
from .modify import Modify
|
||||||
|
from .key import Key
|
||||||
|
|
||||||
|
parser = get_argparser()
|
267
repolib/usr/lib/python3/dist-packages/repolib/command/add.py
Normal file
267
repolib/usr/lib/python3/dist-packages/repolib/command/add.py
Normal file
@ -0,0 +1,267 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
"""
|
||||||
|
Copyright (c) 2022, Ian Santopietro
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
This file is part of RepoLib.
|
||||||
|
|
||||||
|
RepoLib is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
RepoLib is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public License
|
||||||
|
along with RepoLib. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from httplib2.error import ServerNotFoundError
|
||||||
|
from urllib.error import URLError
|
||||||
|
|
||||||
|
from .. import util
|
||||||
|
from ..source import Source, SourceError
|
||||||
|
from ..file import SourceFile, SourceFileError
|
||||||
|
from ..shortcuts import ppa, popdev, shortcut_prefixes
|
||||||
|
from .command import Command, RepolibCommandError
|
||||||
|
|
||||||
|
class Add(Command):
|
||||||
|
"""Add subcommand
|
||||||
|
|
||||||
|
Adds a new source into the system. Requests root, if not present.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--disable, d
|
||||||
|
--source-code, -s
|
||||||
|
--terse, -t
|
||||||
|
--name, -n
|
||||||
|
--identifier, -i
|
||||||
|
--format, -f
|
||||||
|
--skip-keys, -k
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def init_options(cls, subparsers):
|
||||||
|
""" Sets up the argument parser for this command.
|
||||||
|
|
||||||
|
Returns: argparse.subparser
|
||||||
|
The subparser for this command
|
||||||
|
"""
|
||||||
|
|
||||||
|
sub = subparsers.add_parser(
|
||||||
|
'add',
|
||||||
|
help='Add a new repository to the system'
|
||||||
|
)
|
||||||
|
|
||||||
|
sub.add_argument(
|
||||||
|
'deb_line',
|
||||||
|
nargs='*',
|
||||||
|
default=['822styledeb'],
|
||||||
|
help='The deb line of the repository to add'
|
||||||
|
)
|
||||||
|
sub.add_argument(
|
||||||
|
'-d',
|
||||||
|
'--disable',
|
||||||
|
action='store_true',
|
||||||
|
help='Add the repository and then set it to disabled.'
|
||||||
|
)
|
||||||
|
sub.add_argument(
|
||||||
|
'-s',
|
||||||
|
'--source-code',
|
||||||
|
action='store_true',
|
||||||
|
help='Also enable source code packages for the repository.'
|
||||||
|
)
|
||||||
|
sub.add_argument(
|
||||||
|
'-t',
|
||||||
|
'--terse',
|
||||||
|
action='store_true',
|
||||||
|
help='Do not display expanded info about a repository before adding it.'
|
||||||
|
)
|
||||||
|
sub.add_argument(
|
||||||
|
'-n',
|
||||||
|
'--name',
|
||||||
|
default='',
|
||||||
|
help='A name to set for the new repo'
|
||||||
|
)
|
||||||
|
sub.add_argument(
|
||||||
|
'-i',
|
||||||
|
'--identifier',
|
||||||
|
default='',
|
||||||
|
help='The filename to use for the new source'
|
||||||
|
)
|
||||||
|
sub.add_argument(
|
||||||
|
'-f',
|
||||||
|
'--format',
|
||||||
|
default='sources',
|
||||||
|
help='The source format to save as, `sources` or `list`'
|
||||||
|
)
|
||||||
|
sub.add_argument(
|
||||||
|
'-k',
|
||||||
|
'--skip-keys',
|
||||||
|
action='store_true',
|
||||||
|
help='Skip adding signing keys (not recommended!)'
|
||||||
|
)
|
||||||
|
|
||||||
|
return sub
|
||||||
|
|
||||||
|
def finalize_options(self, args):
|
||||||
|
super().finalize_options(args)
|
||||||
|
self.deb_line = ' '.join(args.deb_line)
|
||||||
|
self.terse = args.terse
|
||||||
|
self.source_code = args.source_code
|
||||||
|
self.disable = args.disable
|
||||||
|
self.log.debug(args)
|
||||||
|
|
||||||
|
try:
|
||||||
|
name = args.name.split()
|
||||||
|
except AttributeError:
|
||||||
|
name = args.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
ident = args.identifier.split()
|
||||||
|
except AttributeError:
|
||||||
|
ident = args.identifier
|
||||||
|
|
||||||
|
self.name = ' '.join(name)
|
||||||
|
pre_ident:str = '-'.join(ident)
|
||||||
|
self.ident = util.scrub_filename(pre_ident)
|
||||||
|
self.skip_keys = args.skip_keys
|
||||||
|
self.format = args.format.lower()
|
||||||
|
|
||||||
|
def run(self) -> bool:
|
||||||
|
"""Run the command, return `True` if successful; otherwise `False`"""
|
||||||
|
if self.deb_line == '822styledeb':
|
||||||
|
self.parser.print_usage()
|
||||||
|
self.log.error('A repository is required')
|
||||||
|
return False
|
||||||
|
|
||||||
|
repo_is_url = self.deb_line.startswith('http')
|
||||||
|
repo_is_nospaced = len(self.deb_line.split()) == 1
|
||||||
|
|
||||||
|
if repo_is_url and repo_is_nospaced:
|
||||||
|
self.deb_line = f'deb {self.deb_line} {util.DISTRO_CODENAME} main'
|
||||||
|
|
||||||
|
print('Fetching repository information...')
|
||||||
|
|
||||||
|
self.log.debug('Adding line %s', self.deb_line)
|
||||||
|
|
||||||
|
new_source: Source = Source()
|
||||||
|
|
||||||
|
for prefix in shortcut_prefixes:
|
||||||
|
self.log.debug('Trying prefix %s', prefix)
|
||||||
|
if self.deb_line.startswith(prefix):
|
||||||
|
self.log.debug('Line is prefix: %s', prefix)
|
||||||
|
new_source = shortcut_prefixes[prefix]()
|
||||||
|
if not new_source.validator(self.deb_line):
|
||||||
|
self.log.error(
|
||||||
|
'The shortcut "%s" is malformed',
|
||||||
|
self.deb_line
|
||||||
|
)
|
||||||
|
|
||||||
|
# Try and get a suggested correction for common errors
|
||||||
|
try:
|
||||||
|
if self.deb_line[len(prefix)] != ':':
|
||||||
|
fixed_debline: str = self.deb_line.replace(
|
||||||
|
self.deb_line[len(prefix)],
|
||||||
|
":",
|
||||||
|
1
|
||||||
|
)
|
||||||
|
print(f'Did you mean "{fixed_debline}"?')
|
||||||
|
except IndexError:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
new_source.load_from_data([self.deb_line])
|
||||||
|
except (URLError, ServerNotFoundError) as err:
|
||||||
|
import traceback
|
||||||
|
self.log.debug(
|
||||||
|
'Exception info: %s \n %s \n %s',
|
||||||
|
type(err),
|
||||||
|
''.join(traceback.format_exception(err)),
|
||||||
|
err.args
|
||||||
|
)
|
||||||
|
self.log.error(
|
||||||
|
'System is offline. A connection is required to add '
|
||||||
|
'PPA and Popdev sources.'
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
except Exception as err:
|
||||||
|
import traceback
|
||||||
|
self.log.debug(
|
||||||
|
'Exception info: %s \n %s \n %s',
|
||||||
|
type(err),
|
||||||
|
err.__traceback__,
|
||||||
|
err
|
||||||
|
)
|
||||||
|
self.log.error('An error ocurred: %s', err)
|
||||||
|
return False
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
if not new_source:
|
||||||
|
self.log.error(
|
||||||
|
f'Could not parse line "{self.deb_line}". Double-check the '
|
||||||
|
'spelling.'
|
||||||
|
)
|
||||||
|
valid_shortcuts: str = ''
|
||||||
|
for shortcut in shortcut_prefixes:
|
||||||
|
if shortcut.startswith('deb'):
|
||||||
|
continue
|
||||||
|
valid_shortcuts += f'{shortcut}, '
|
||||||
|
valid_shortcuts = valid_shortcuts.strip(', ')
|
||||||
|
print(f'Supported repository shortcuts:\n {valid_shortcuts}')
|
||||||
|
return False
|
||||||
|
|
||||||
|
new_source.twin_source = True
|
||||||
|
new_source.sourcecode_enabled = self.source_code
|
||||||
|
|
||||||
|
if self.name:
|
||||||
|
new_source.name = self.name
|
||||||
|
if self.ident:
|
||||||
|
new_source.ident = self.ident
|
||||||
|
if self.disable:
|
||||||
|
new_source.enabled = False
|
||||||
|
|
||||||
|
if not new_source.ident:
|
||||||
|
new_source.ident = new_source.generate_default_ident()
|
||||||
|
|
||||||
|
new_file = SourceFile(name=new_source.ident)
|
||||||
|
new_file.format = new_source.default_format
|
||||||
|
if self.format:
|
||||||
|
self.log.info('Setting new source format to %s', self.format)
|
||||||
|
for format in util.SourceFormat:
|
||||||
|
if self.format == format.value:
|
||||||
|
new_file.format = format
|
||||||
|
self.log.debug('New source format set to %s', format.value)
|
||||||
|
|
||||||
|
new_file.add_source(new_source)
|
||||||
|
new_source.file = new_file
|
||||||
|
self.log.debug('File format: %s', new_file.format)
|
||||||
|
self.log.debug('File path: %s', new_file.path)
|
||||||
|
|
||||||
|
self.log.debug('Sources in file %s:\n%s', new_file.path, new_file.sources)
|
||||||
|
|
||||||
|
if not self.terse:
|
||||||
|
print(
|
||||||
|
'Adding the following source: \n',
|
||||||
|
new_source.get_description(),
|
||||||
|
'\n\n',
|
||||||
|
new_source.ui
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
input(
|
||||||
|
'Press ENTER to continue adding this source or Ctrl+C '
|
||||||
|
'to cancel'
|
||||||
|
)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
# Handled here to avoid printing the exception to console
|
||||||
|
exit(0)
|
||||||
|
|
||||||
|
new_file.save()
|
||||||
|
util.dbus_quit()
|
||||||
|
return True
|
@ -0,0 +1,65 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
"""
|
||||||
|
Copyright (c) 2020, Ian Santopietro
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
This file is part of RepoLib.
|
||||||
|
|
||||||
|
RepoLib is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
RepoLib is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public License
|
||||||
|
along with RepoLib. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Module for getting an argparser. Used by apt-manage
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
from repolib import command as cmd
|
||||||
|
|
||||||
|
from .. import __version__
|
||||||
|
|
||||||
|
def get_argparser():
|
||||||
|
""" Get an argument parser with our arguments.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
An argparse.ArgumentParser.
|
||||||
|
"""
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
prog='apt-manage',
|
||||||
|
description='Manage software sources and repositories',
|
||||||
|
epilog='apt-manage version: {}'.format(__version__.__version__)
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'-b',
|
||||||
|
'--debug',
|
||||||
|
action='count',
|
||||||
|
help=argparse.SUPPRESS
|
||||||
|
)
|
||||||
|
|
||||||
|
subparsers = parser.add_subparsers(
|
||||||
|
help='...',
|
||||||
|
dest='action',
|
||||||
|
metavar='COMMAND'
|
||||||
|
)
|
||||||
|
|
||||||
|
subcommands = []
|
||||||
|
for i in inspect.getmembers(cmd, inspect.isclass):
|
||||||
|
obj = getattr(cmd, i[0])
|
||||||
|
subcommands.append(obj)
|
||||||
|
|
||||||
|
for i in subcommands:
|
||||||
|
i.init_options(subparsers)
|
||||||
|
|
||||||
|
return parser
|
@ -0,0 +1,77 @@
|
|||||||
|
"""
|
||||||
|
Copyright (c) 2020, Ian Santopietro
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
This file is part of RepoLib.
|
||||||
|
|
||||||
|
RepoLib is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
RepoLib is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public License
|
||||||
|
along with RepoLib. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Command line application helper class.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from ..util import SOURCES_DIR
|
||||||
|
|
||||||
|
class RepolibCommandError(Exception):
|
||||||
|
""" Exceptions generated by Repolib CLIs."""
|
||||||
|
|
||||||
|
def __init__(self, *args, code=1, **kwargs):
|
||||||
|
"""Exception from a CLI
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
code (:obj:`int`, optional, default=1): Exception error code.
|
||||||
|
"""
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.code = code
|
||||||
|
|
||||||
|
class Command:
|
||||||
|
# pylint: disable=no-self-use,too-few-public-methods
|
||||||
|
# This is a base class for other things to inherit and give other programs
|
||||||
|
# a standardized interface for interacting with commands.
|
||||||
|
""" CLI helper class for developing command line applications."""
|
||||||
|
|
||||||
|
def __init__(self, log, args, parser):
|
||||||
|
self.log = log
|
||||||
|
self.parser = parser
|
||||||
|
self.sources_dir = SOURCES_DIR
|
||||||
|
self.finalize_options(args)
|
||||||
|
|
||||||
|
def finalize_options(self, args):
|
||||||
|
""" Base options parsing class.
|
||||||
|
|
||||||
|
Use this class in commands to set up the final options parsing and set
|
||||||
|
instance variables for later use.
|
||||||
|
"""
|
||||||
|
self.verbose = False
|
||||||
|
self.debug = False
|
||||||
|
if args.debug:
|
||||||
|
if args.debug > 1:
|
||||||
|
self.verbose = True
|
||||||
|
if args.debug != 0:
|
||||||
|
self.log.info('Debug mode set, not-saving any changes.')
|
||||||
|
self.debug = True
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
""" The initial base for running the command.
|
||||||
|
|
||||||
|
This needs to have a standardized format, argument list, and return
|
||||||
|
either True or False depending on whether the command was successful or
|
||||||
|
not.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if the command succeeded, otherwise False.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Since this is just the base, it should always return True (because
|
||||||
|
# you shouldn't fail at doing nothing).
|
||||||
|
return True
|
375
repolib/usr/lib/python3/dist-packages/repolib/command/key.py
Normal file
375
repolib/usr/lib/python3/dist-packages/repolib/command/key.py
Normal file
@ -0,0 +1,375 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
"""
|
||||||
|
Copyright (c) 2022, Ian Santopietro
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
This file is part of RepoLib.
|
||||||
|
|
||||||
|
RepoLib is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
RepoLib is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public License
|
||||||
|
along with RepoLib. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from ..key import SourceKey
|
||||||
|
from .. import system, util
|
||||||
|
|
||||||
|
from .command import Command
|
||||||
|
|
||||||
|
KEYS_PATH = Path(util.KEYS_DIR)
|
||||||
|
|
||||||
|
class Key(Command):
|
||||||
|
"""Key subcommand.
|
||||||
|
|
||||||
|
Manages signing keys for repositories, allowing adding, removal, and
|
||||||
|
fetching of remote keys.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--name, -n
|
||||||
|
--path, -p
|
||||||
|
--url, -u
|
||||||
|
--ascii, -a
|
||||||
|
--fingerprint, -f
|
||||||
|
--keyserver, -s
|
||||||
|
--remove, -r
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def init_options(cls, subparsers) -> None:
|
||||||
|
"""Sets up this command's options parser"""
|
||||||
|
|
||||||
|
sub = subparsers.add_parser(
|
||||||
|
'key',
|
||||||
|
help='Manage repository signing keys',
|
||||||
|
epilog=(
|
||||||
|
'Note that no verification of key validity is performed when '
|
||||||
|
'managing key using apt-mange. Ensure that keys are valid to '
|
||||||
|
'avoid errors with updates.'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
sub.add_argument(
|
||||||
|
'repository',
|
||||||
|
default=['x-repolib-none'],
|
||||||
|
help='Which repository to manage keys for.'
|
||||||
|
)
|
||||||
|
|
||||||
|
sub_group = sub.add_mutually_exclusive_group()
|
||||||
|
|
||||||
|
sub_group.add_argument(
|
||||||
|
'-n',
|
||||||
|
'--name',
|
||||||
|
help='The name of an existing key file to set.'
|
||||||
|
)
|
||||||
|
|
||||||
|
sub_group.add_argument(
|
||||||
|
'-p',
|
||||||
|
'--path',
|
||||||
|
help='Sets a path on disk to a signing key.'
|
||||||
|
)
|
||||||
|
|
||||||
|
sub_group.add_argument(
|
||||||
|
'-u',
|
||||||
|
'--url',
|
||||||
|
help='Download a key over HTTPS'
|
||||||
|
)
|
||||||
|
|
||||||
|
sub_group.add_argument(
|
||||||
|
'-a',
|
||||||
|
'--ascii',
|
||||||
|
help='Add an ascii-armored key'
|
||||||
|
)
|
||||||
|
|
||||||
|
sub_group.add_argument(
|
||||||
|
'-f',
|
||||||
|
'--fingerprint',
|
||||||
|
help=(
|
||||||
|
'Fetch a key via fingerprint from a keyserver. Use --keyserver '
|
||||||
|
'to specify the URL to the keyserver.'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
sub_group.add_argument(
|
||||||
|
'-i',
|
||||||
|
'--info',
|
||||||
|
action='store_true',
|
||||||
|
help='Print key information'
|
||||||
|
)
|
||||||
|
|
||||||
|
sub_group.add_argument(
|
||||||
|
'-r',
|
||||||
|
'--remove',
|
||||||
|
action='store_true',
|
||||||
|
help=(
|
||||||
|
'Remove a signing key from the repo. If no other sources use '
|
||||||
|
'this key, its file will be deleted.'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
sub.add_argument(
|
||||||
|
'-s',
|
||||||
|
'--keyserver',
|
||||||
|
help=(
|
||||||
|
'The keyserver from which to fetch the given fingerprint. '
|
||||||
|
'(Default: keyserver.ubuntu.com)'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def finalize_options(self, args):
|
||||||
|
super().finalize_options(args)
|
||||||
|
self.repo = args.repository
|
||||||
|
self.keyserver = args.keyserver
|
||||||
|
|
||||||
|
self.actions:dict = {}
|
||||||
|
self.system_source = False
|
||||||
|
|
||||||
|
for act in [
|
||||||
|
'name',
|
||||||
|
'path',
|
||||||
|
'url',
|
||||||
|
'ascii',
|
||||||
|
'fingerprint',
|
||||||
|
'info',
|
||||||
|
'remove',
|
||||||
|
]:
|
||||||
|
self.actions[act] = getattr(args, act)
|
||||||
|
|
||||||
|
system.load_all_sources()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""Run the command"""
|
||||||
|
self.log.info('Modifying signing key settings for %s', self.repo)
|
||||||
|
|
||||||
|
if not self.repo:
|
||||||
|
self.log.error('No repository provided')
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.source = util.sources[self.repo]
|
||||||
|
if self.repo == 'system':
|
||||||
|
self.system_source = True
|
||||||
|
except KeyError:
|
||||||
|
self.log.error(
|
||||||
|
'The repository %s was not found. Check the spelling',
|
||||||
|
self.repo
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.log.debug('Actions to take:\n%s', self.actions)
|
||||||
|
# Run info, unless a different action is specified
|
||||||
|
self.actions['info'] = True
|
||||||
|
for key in self.actions:
|
||||||
|
if key == 'info':
|
||||||
|
self.log.debug('Skipping info key')
|
||||||
|
continue
|
||||||
|
if self.actions[key]:
|
||||||
|
self.log.info('Got an action, skipping info')
|
||||||
|
self.actions['info'] = False
|
||||||
|
break
|
||||||
|
|
||||||
|
self.log.debug('Source before:\n%s', self.source)
|
||||||
|
|
||||||
|
rets = []
|
||||||
|
for action in self.actions:
|
||||||
|
if self.actions[action]:
|
||||||
|
self.log.debug('Running action: %s - (%s)', action, self.actions[action])
|
||||||
|
ret = getattr(self, action)(self.actions[action])
|
||||||
|
rets.append(ret)
|
||||||
|
break
|
||||||
|
|
||||||
|
self.log.debug('Results: %s', rets)
|
||||||
|
self.log.debug('Source after: \n%s', self.source)
|
||||||
|
|
||||||
|
if self.actions['info']:
|
||||||
|
self.log.info('Running Info, skipping saving %s', self.source.ident)
|
||||||
|
return True
|
||||||
|
|
||||||
|
if True in rets:
|
||||||
|
self.log.info('Saving source %s', self.source.ident)
|
||||||
|
self.source.file.save()
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
self.log.warning('No valid changes specified, no actions taken.')
|
||||||
|
return False
|
||||||
|
|
||||||
|
def name(self, value:str) -> bool:
|
||||||
|
"""Sets the key file to a name in the key file directory"""
|
||||||
|
if not value:
|
||||||
|
return False
|
||||||
|
|
||||||
|
key_path = None
|
||||||
|
|
||||||
|
for ext in ['.gpg', '.asc']:
|
||||||
|
if value.endswith(ext):
|
||||||
|
path = KEYS_PATH / value
|
||||||
|
if path.exists():
|
||||||
|
key_path = path
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
if 'archive-keyring' not in value:
|
||||||
|
value += '-archive-keyring'
|
||||||
|
path = KEYS_PATH / f'{value}{ext}'
|
||||||
|
if path.exists():
|
||||||
|
key_path = path
|
||||||
|
break
|
||||||
|
|
||||||
|
if not key_path:
|
||||||
|
self.log.error('The key file %s could not be found', value)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return self.path(str(key_path))
|
||||||
|
|
||||||
|
def path(self, value:str) -> bool:
|
||||||
|
"""Sets the key file to the given path"""
|
||||||
|
if not value:
|
||||||
|
return False
|
||||||
|
|
||||||
|
key_path = Path(value)
|
||||||
|
|
||||||
|
if not key_path.exists():
|
||||||
|
self.log.error(
|
||||||
|
'The path %s does not exist', value
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.source.signed_by = str(key_path)
|
||||||
|
self.source.load_key()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def url(self, value:str) -> bool:
|
||||||
|
"""Downloads a key to use from a provided URL."""
|
||||||
|
if not value:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not value.startswith('https:'):
|
||||||
|
response = 'n'
|
||||||
|
self.log.warning(
|
||||||
|
'The key is not being downloaded over an encrypted connection, '
|
||||||
|
'and may be tampered with in-transit. Only add keys that you '
|
||||||
|
'trust, from sources which you trust.'
|
||||||
|
)
|
||||||
|
response = input('Do you want to continue? (y/N): ')
|
||||||
|
if response not in util.true_values:
|
||||||
|
return False
|
||||||
|
|
||||||
|
key = SourceKey(name=self.source.ident)
|
||||||
|
key.load_key_data(url=value)
|
||||||
|
self.source.key = key
|
||||||
|
self.source.signed_by = str(key.path)
|
||||||
|
self.source.load_key()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def ascii(self, value:str) -> bool:
|
||||||
|
"""Loads the key from provided ASCII-armored data"""
|
||||||
|
if not value:
|
||||||
|
return False
|
||||||
|
|
||||||
|
response = 'n'
|
||||||
|
print('Only add signing keys from data you trust.')
|
||||||
|
response = input('Do you want to continue? (y/N): ')
|
||||||
|
|
||||||
|
if response not in util.true_values:
|
||||||
|
return False
|
||||||
|
|
||||||
|
key = SourceKey(name=self.source.ident)
|
||||||
|
key.load_key_data(ascii=value)
|
||||||
|
self.source.key = key
|
||||||
|
self.source.signed_by = str(key.path)
|
||||||
|
self.source.load_key()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def fingerprint(self, value:str) -> bool:
|
||||||
|
"""Downloads the key with the given fingerprint from a keyserver"""
|
||||||
|
if not value:
|
||||||
|
return False
|
||||||
|
|
||||||
|
key = SourceKey(name=self.source.ident)
|
||||||
|
|
||||||
|
if self.keyserver:
|
||||||
|
key.load_key_data(fingerprint=value, keyserver=self.keyserver)
|
||||||
|
else:
|
||||||
|
key.load_key_data(fingerprint=value)
|
||||||
|
|
||||||
|
self.source.key = key
|
||||||
|
self.source.signed_by = str(key.path)
|
||||||
|
self.source.load_key()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def info(self, value:str) -> bool:
|
||||||
|
"""Prints key information"""
|
||||||
|
from datetime import date
|
||||||
|
if not self.source.key:
|
||||||
|
self.log.error(
|
||||||
|
'The source %s does not have a key configured.',
|
||||||
|
self.repo
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
else:
|
||||||
|
key:dict = self.source.get_key_info()
|
||||||
|
output: str = f'Key information for source {self.source.ident}:\n'
|
||||||
|
output += f'Key ID: {key["uids"][0]}\n'
|
||||||
|
output += f'Fingerprint: {key["keyid"]}\n'
|
||||||
|
if key['type'] == 'pub':
|
||||||
|
output += 'Key Type: Public\n'
|
||||||
|
else:
|
||||||
|
output += 'Key Type: Private\n'
|
||||||
|
keydate = date.fromtimestamp(int(key['date']))
|
||||||
|
output += f'Issue Date: {keydate.ctime()}\n'
|
||||||
|
output += f'Length: {key["length"]} Bytes\n'
|
||||||
|
output += f'Keyring Path: {str(self.source.key.path)}\n'
|
||||||
|
print(output)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def remove(self, value:str) -> bool:
|
||||||
|
"""Removes the key from the source"""
|
||||||
|
|
||||||
|
if not self.source.key:
|
||||||
|
self.log.error(
|
||||||
|
'The source %s does not have a key configured.',
|
||||||
|
self.repo
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
response = 'n'
|
||||||
|
print(
|
||||||
|
'If you remove the key, there may no longer be any verification '
|
||||||
|
'of software packages from this repository, including for future '
|
||||||
|
'updates. This may cause errors with your updates.'
|
||||||
|
)
|
||||||
|
response = input('Do you want to continue? (y/N): ')
|
||||||
|
if response not in util.true_values:
|
||||||
|
return False
|
||||||
|
|
||||||
|
old_key = self.source.key
|
||||||
|
self.source.key = None
|
||||||
|
self.source.signed_by = ''
|
||||||
|
|
||||||
|
for source in util.sources.values():
|
||||||
|
if source.key == old_key:
|
||||||
|
self.log.info(
|
||||||
|
'Key file %s in use with another key, not deleting',
|
||||||
|
old_key.path
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
response = 'n'
|
||||||
|
print('No other sources were found which use this key.')
|
||||||
|
response = input('Do you want to remove it? (Y/n): ')
|
||||||
|
if response in util.true_values:
|
||||||
|
old_key.delete_key()
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
216
repolib/usr/lib/python3/dist-packages/repolib/command/list.py
Normal file
216
repolib/usr/lib/python3/dist-packages/repolib/command/list.py
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
"""
|
||||||
|
Copyright (c) 2022, Ian Santopietro
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
This file is part of RepoLib.
|
||||||
|
|
||||||
|
RepoLib is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
RepoLib is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public License
|
||||||
|
along with RepoLib. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import textwrap
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
from ..file import SourceFile, SourceFileError
|
||||||
|
from ..source import Source, SourceError
|
||||||
|
from .. import RepoError, util, system
|
||||||
|
|
||||||
|
from .command import Command, RepolibCommandError
|
||||||
|
|
||||||
|
class List(Command):
|
||||||
|
"""List subcommand
|
||||||
|
|
||||||
|
Lists information about currently-configured sources on the system.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--legacy, -l
|
||||||
|
--verbose, -v
|
||||||
|
--all, -a
|
||||||
|
--no-names, -n
|
||||||
|
--file-names, -f
|
||||||
|
--no-indentation
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def init_options(cls, subparsers):
|
||||||
|
"""Sets up ths argument parser for this command.
|
||||||
|
|
||||||
|
Returns: argparse.subparser:
|
||||||
|
This command's subparser
|
||||||
|
"""
|
||||||
|
|
||||||
|
sub = subparsers.add_parser(
|
||||||
|
'list',
|
||||||
|
help=(
|
||||||
|
'List information for configured repostiories. If a repository '
|
||||||
|
'name is provided, details about that repository are printed.'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
sub.add_argument(
|
||||||
|
'repository',
|
||||||
|
nargs='*',
|
||||||
|
default=['x-repolib-all-sources'],
|
||||||
|
help='The repository for which to list configuration'
|
||||||
|
)
|
||||||
|
sub.add_argument(
|
||||||
|
'-v',
|
||||||
|
'--verbose',
|
||||||
|
action='store_true',
|
||||||
|
help='Show additional information, if available.'
|
||||||
|
)
|
||||||
|
sub.add_argument(
|
||||||
|
'-a',
|
||||||
|
'--all',
|
||||||
|
action='store_true',
|
||||||
|
help='Display full configuration for all configured repositories.'
|
||||||
|
)
|
||||||
|
sub.add_argument(
|
||||||
|
'-l',
|
||||||
|
'--legacy',
|
||||||
|
action='store_true',
|
||||||
|
help='Include repositories configured in legacy sources.list file.'
|
||||||
|
)
|
||||||
|
sub.add_argument(
|
||||||
|
'-n',
|
||||||
|
'--no-names',
|
||||||
|
action='store_true',
|
||||||
|
dest='skip_names',
|
||||||
|
help=argparse.SUPPRESS
|
||||||
|
)
|
||||||
|
sub.add_argument(
|
||||||
|
'-f',
|
||||||
|
'--file-names',
|
||||||
|
action='store_true',
|
||||||
|
dest='print_files',
|
||||||
|
help="Don't print names of files"
|
||||||
|
)
|
||||||
|
sub.add_argument(
|
||||||
|
'--no-indentation',
|
||||||
|
action='store_true',
|
||||||
|
dest='no_indent',
|
||||||
|
help=argparse.SUPPRESS
|
||||||
|
)
|
||||||
|
|
||||||
|
def finalize_options(self, args):
|
||||||
|
super().finalize_options(args)
|
||||||
|
self.repo = ' '.join(args.repository)
|
||||||
|
self.verbose = args.verbose
|
||||||
|
self.all = args.all
|
||||||
|
self.legacy = args.legacy
|
||||||
|
self.skip_names = args.skip_names
|
||||||
|
self.print_files = args.print_files
|
||||||
|
self.no_indent = args.no_indent
|
||||||
|
|
||||||
|
def list_legacy(self, indent) -> None:
|
||||||
|
"""List the contents of the sources.list file.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
list_file(Path): The sources.list file to try and parse.
|
||||||
|
indent(str): An indentation to append to the output
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
sources_list_file = util.SOURCES_DIR.parent / 'sources.list'
|
||||||
|
except FileNotFoundError:
|
||||||
|
sources_list_file = None
|
||||||
|
|
||||||
|
print('Legacy source.list sources:')
|
||||||
|
if sources_list_file:
|
||||||
|
with open(sources_list_file, mode='r') as file:
|
||||||
|
for line in file:
|
||||||
|
if 'cdrom' in line:
|
||||||
|
line = ''
|
||||||
|
|
||||||
|
try:
|
||||||
|
source = Source()
|
||||||
|
source.load_from_data([line])
|
||||||
|
print(textwrap.indent(source.ui, indent))
|
||||||
|
except SourceError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def list_all(self):
|
||||||
|
"""List all sources presently configured in the system
|
||||||
|
|
||||||
|
This may include the configuration data for each source as well
|
||||||
|
"""
|
||||||
|
|
||||||
|
indent = ' '
|
||||||
|
if self.no_indent:
|
||||||
|
indent = ''
|
||||||
|
|
||||||
|
if self.print_files:
|
||||||
|
print('Configured source files:')
|
||||||
|
|
||||||
|
for file in util.files:
|
||||||
|
print(f'{file.path.name}:')
|
||||||
|
|
||||||
|
for source in file.sources:
|
||||||
|
print(textwrap.indent(source.ui, indent))
|
||||||
|
|
||||||
|
if self.legacy:
|
||||||
|
self.list_legacy(indent)
|
||||||
|
|
||||||
|
else:
|
||||||
|
print('Configured Sources:')
|
||||||
|
for source in util.sources:
|
||||||
|
output = util.sources[source]
|
||||||
|
print(textwrap.indent(output.ui, indent))
|
||||||
|
|
||||||
|
if self.legacy:
|
||||||
|
self.list_legacy(indent)
|
||||||
|
|
||||||
|
if util.errors:
|
||||||
|
print('\n\nThe following files contain formatting errors:')
|
||||||
|
for err in util.errors:
|
||||||
|
print(err)
|
||||||
|
if self.verbose or self.debug:
|
||||||
|
print('\nDetails about failing files:')
|
||||||
|
for err in util.errors:
|
||||||
|
print(f'{err}: {util.errors[err].args[0]}')
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""Run the command"""
|
||||||
|
system.load_all_sources()
|
||||||
|
self.log.debug("Current sources: %s", util.sources)
|
||||||
|
ret = False
|
||||||
|
|
||||||
|
if self.all:
|
||||||
|
return self.list_all()
|
||||||
|
|
||||||
|
if self.repo == 'x-repolib-all-sources' and not self.all:
|
||||||
|
if not self.skip_names:
|
||||||
|
print('Configured Sources:')
|
||||||
|
for source in util.sources:
|
||||||
|
line = source
|
||||||
|
if not self.skip_names:
|
||||||
|
line += f' - {util.sources[source].name}'
|
||||||
|
print(line)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
output = util.sources[self.repo]
|
||||||
|
print(f'Details for source {output.ui}')
|
||||||
|
return True
|
||||||
|
except KeyError:
|
||||||
|
self.log.error(
|
||||||
|
"Couldn't find the source file for %s, check the spelling",
|
||||||
|
self.repo
|
||||||
|
)
|
||||||
|
return False
|
502
repolib/usr/lib/python3/dist-packages/repolib/command/modify.py
Normal file
502
repolib/usr/lib/python3/dist-packages/repolib/command/modify.py
Normal file
@ -0,0 +1,502 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
"""
|
||||||
|
Copyright (c) 2022, Ian Santopietro
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
This file is part of RepoLib.
|
||||||
|
|
||||||
|
RepoLib is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
RepoLib is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public License
|
||||||
|
along with RepoLib. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from argparse import SUPPRESS
|
||||||
|
|
||||||
|
from .command import Command, RepolibCommandError
|
||||||
|
from .. import util, system
|
||||||
|
|
||||||
|
class Modify(Command):
|
||||||
|
"""Modify Subcommand
|
||||||
|
|
||||||
|
Makes modifications to the specified repository
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--enable, -e
|
||||||
|
--disable, -d
|
||||||
|
--default-mirror
|
||||||
|
--name
|
||||||
|
--add-suite
|
||||||
|
--remove-suite
|
||||||
|
--add-component
|
||||||
|
--remove-component
|
||||||
|
--add-uri
|
||||||
|
--remove-uri
|
||||||
|
|
||||||
|
Hidden Options
|
||||||
|
--add-option
|
||||||
|
--remove-option
|
||||||
|
--default-mirror
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def init_options(cls, subparsers):
|
||||||
|
""" Sets up this command's options parser"""
|
||||||
|
|
||||||
|
sub = subparsers.add_parser(
|
||||||
|
'modify',
|
||||||
|
help='Change a configured repository.'
|
||||||
|
)
|
||||||
|
sub.add_argument(
|
||||||
|
'repository',
|
||||||
|
nargs='*',
|
||||||
|
default=['system'],
|
||||||
|
help='The repository to modify. Default is the system repository.'
|
||||||
|
)
|
||||||
|
|
||||||
|
modify_enable = sub.add_mutually_exclusive_group(
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
modify_enable.add_argument(
|
||||||
|
'-e',
|
||||||
|
'--enable',
|
||||||
|
action='store_true',
|
||||||
|
help='Enable the repository, if disabled.'
|
||||||
|
)
|
||||||
|
modify_enable.add_argument(
|
||||||
|
'-d',
|
||||||
|
'--disable',
|
||||||
|
action='store_true',
|
||||||
|
help=(
|
||||||
|
'Disable the repository, if enabled. The system repository cannot '
|
||||||
|
'be disabled.'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
modify_source = sub.add_mutually_exclusive_group(
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
modify_source.add_argument(
|
||||||
|
'--source-enable',
|
||||||
|
action='store_true',
|
||||||
|
help='Enable source code for the repository, if disabled.'
|
||||||
|
)
|
||||||
|
modify_source.add_argument(
|
||||||
|
'--source-disable',
|
||||||
|
action='store_true',
|
||||||
|
help='Disable source code for the repository, if enabled.'
|
||||||
|
)
|
||||||
|
|
||||||
|
sub.add_argument(
|
||||||
|
'--default-mirror',
|
||||||
|
help=SUPPRESS
|
||||||
|
#help='Sets the default mirror for the system source.'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Name
|
||||||
|
sub.add_argument(
|
||||||
|
'-n',
|
||||||
|
'--name',
|
||||||
|
help='Set the repository name to NAME'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Suites
|
||||||
|
sub.add_argument(
|
||||||
|
'--add-suite',
|
||||||
|
metavar='SUITE[,SUITE]',
|
||||||
|
help=(
|
||||||
|
'Add the specified suite(s) to the repository. Multiple '
|
||||||
|
'repositories should be separated with commas. NOTE: Legacy deb '
|
||||||
|
'repositories may only contain one suite.'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
sub.add_argument(
|
||||||
|
'--remove-suite',
|
||||||
|
metavar='SUITE[,SUITE]',
|
||||||
|
help=(
|
||||||
|
'Remove the specified suite(s) from the repository. Multiple '
|
||||||
|
'repositories should be separated with commas. NOTE: The last '
|
||||||
|
'suite in a repository cannot be removed.'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Components
|
||||||
|
sub.add_argument(
|
||||||
|
'--add-component',
|
||||||
|
metavar='COMPONENT[,COMPONENT]',
|
||||||
|
help=(
|
||||||
|
'Add the specified component(s) to the repository. Multiple '
|
||||||
|
'repositories should be separated with commas.'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
sub.add_argument(
|
||||||
|
'--remove-component',
|
||||||
|
metavar='COMPONENT[,COMPONENT]',
|
||||||
|
help=(
|
||||||
|
'Remove the specified component(s) from the repository. Multiple '
|
||||||
|
'repositories should be separated with commas. NOTE: The last '
|
||||||
|
'component in a repository cannot be removed.'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# URIs
|
||||||
|
sub.add_argument(
|
||||||
|
'--add-uri',
|
||||||
|
metavar='URI[,URI]',
|
||||||
|
help=(
|
||||||
|
'Add the specified URI(s) to the repository. Multiple '
|
||||||
|
'repositories should be separated with commas. NOTE: Legacy deb '
|
||||||
|
'repositories may only contain one uri.'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
sub.add_argument(
|
||||||
|
'--remove-uri',
|
||||||
|
metavar='URI[,URI]',
|
||||||
|
help=(
|
||||||
|
'Remove the specified URI(s) from the repository. Multiple '
|
||||||
|
'repositories should be separated with commas. NOTE: The last '
|
||||||
|
'uri in a repository cannot be removed.'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Options
|
||||||
|
sub.add_argument(
|
||||||
|
'--add-option',
|
||||||
|
metavar='OPTION=VALUE[,OPTION=VALUE]',
|
||||||
|
help=SUPPRESS
|
||||||
|
)
|
||||||
|
sub.add_argument(
|
||||||
|
'--remove-option',
|
||||||
|
metavar='OPTION=VALUE[,OPTION=VALUE]',
|
||||||
|
help=SUPPRESS
|
||||||
|
)
|
||||||
|
|
||||||
|
def finalize_options(self, args):
|
||||||
|
super().finalize_options(args)
|
||||||
|
self.count = 0
|
||||||
|
self.repo = ' '.join(args.repository)
|
||||||
|
self.enable = args.enable
|
||||||
|
self.disable = args.disable
|
||||||
|
self.source_enable = args.source_enable
|
||||||
|
self.source_disable = args.source_disable
|
||||||
|
|
||||||
|
self.actions:dict = {}
|
||||||
|
|
||||||
|
self.actions['endisable'] = None
|
||||||
|
for i in ['enable', 'disable']:
|
||||||
|
if getattr(args, i):
|
||||||
|
self.actions['endisable'] = i
|
||||||
|
|
||||||
|
self.actions['source_endisable'] = None
|
||||||
|
for i in ['source_enable', 'source_disable']:
|
||||||
|
if getattr(args, i):
|
||||||
|
self.actions['source_endisable'] = i
|
||||||
|
|
||||||
|
for arg in [
|
||||||
|
'default_mirror',
|
||||||
|
'name',
|
||||||
|
'add_uri',
|
||||||
|
'add_suite',
|
||||||
|
'add_component',
|
||||||
|
'remove_uri',
|
||||||
|
'remove_suite',
|
||||||
|
'remove_component',
|
||||||
|
]:
|
||||||
|
self.actions[arg] = getattr(args, arg)
|
||||||
|
|
||||||
|
system.load_all_sources()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""Run the command"""
|
||||||
|
self.log.info('Modifying repository %s', self.repo)
|
||||||
|
|
||||||
|
self.system_source = False
|
||||||
|
try:
|
||||||
|
self.source = util.sources[self.repo]
|
||||||
|
except KeyError:
|
||||||
|
self.log.error(
|
||||||
|
'The source %s could not be found. Check the spelling',
|
||||||
|
self.repo
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.source.ident == 'system':
|
||||||
|
self.system_source = True
|
||||||
|
|
||||||
|
self.log.debug('Actions to take:\n%s', self.actions)
|
||||||
|
self.log.debug('Source before:\n%s', self.source)
|
||||||
|
|
||||||
|
rets = []
|
||||||
|
for action in self.actions:
|
||||||
|
ret = getattr(self, action)(self.actions[action])
|
||||||
|
rets.append(ret)
|
||||||
|
|
||||||
|
self.log.debug('Results: %s', rets)
|
||||||
|
self.log.debug('Source after: \n%s', self.source)
|
||||||
|
|
||||||
|
if True in rets:
|
||||||
|
self.source.file.save()
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
self.log.warning('No valid changes specified, no actions taken.')
|
||||||
|
return False
|
||||||
|
|
||||||
|
def default_mirror(self, value:str) -> bool:
|
||||||
|
"""Checks if this is the system source, then set the default mirror"""
|
||||||
|
if not value:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.system_source:
|
||||||
|
self.source['X-Repolib-Default-Mirror'] = value
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def name(self, value:str) -> bool:
|
||||||
|
"""Sets the source name"""
|
||||||
|
if not value:
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.log.info('Setting name for %s to %s', self.repo, value)
|
||||||
|
self.source.name = value
|
||||||
|
return True
|
||||||
|
|
||||||
|
def endisable(self, value:str) -> bool:
|
||||||
|
"""Enable or disable the source"""
|
||||||
|
if not value:
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.log.info('%sing source %s', value[:-1], self.repo)
|
||||||
|
if value == 'disable':
|
||||||
|
self.source.enabled = False
|
||||||
|
return True
|
||||||
|
|
||||||
|
self.source.enabled = True
|
||||||
|
return True
|
||||||
|
|
||||||
|
def source_endisable(self, value:str) -> bool:
|
||||||
|
"""Enable/disable source code for the repo"""
|
||||||
|
if not value:
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.log.info('%sing source code for source %s', value[7:-1], self.repo)
|
||||||
|
if value == 'source_disable':
|
||||||
|
self.source.sourcecode_enabled = False
|
||||||
|
return True
|
||||||
|
|
||||||
|
self.source.sourcecode_enabled = True
|
||||||
|
return True
|
||||||
|
|
||||||
|
def add_uri(self, values:str) -> bool:
|
||||||
|
"""Adds URIs to the source, if not already present."""
|
||||||
|
if not values:
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.log.info('Adding URIs: %s', values)
|
||||||
|
uris = self.source.uris
|
||||||
|
|
||||||
|
for uri in values.split():
|
||||||
|
if not util.url_validator(uri):
|
||||||
|
raise RepolibCommandError(
|
||||||
|
f'Cannot add URI {uri} to {self.repo}. The URI is '
|
||||||
|
'malformed'
|
||||||
|
)
|
||||||
|
|
||||||
|
if uri not in uris:
|
||||||
|
uris.append(uri)
|
||||||
|
self.log.debug('Added URI %s', uri)
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.log.warning(
|
||||||
|
'The URI %s was already present in %s',
|
||||||
|
uri,
|
||||||
|
self.repo
|
||||||
|
)
|
||||||
|
|
||||||
|
if uris != self.source.uris:
|
||||||
|
self.source.uris = uris
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def remove_uri(self, values:str) -> bool:
|
||||||
|
"""Remove URIs from the soruce, if they are added."""
|
||||||
|
if not values:
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.log.info('Removing URIs %s from source %s', values, self.repo)
|
||||||
|
uris = self.source.uris
|
||||||
|
self.log.debug('Starting uris: %s', uris)
|
||||||
|
|
||||||
|
for uri in values.split():
|
||||||
|
try:
|
||||||
|
uris.remove(uri)
|
||||||
|
self.log.debug('Removed URI %s', uri)
|
||||||
|
|
||||||
|
except ValueError:
|
||||||
|
self.log.warning(
|
||||||
|
'The URI %s was not present in %s',
|
||||||
|
uri,
|
||||||
|
self.repo
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(uris) == 0:
|
||||||
|
self.log.error(
|
||||||
|
'Cannot remove the last URI from %s. If you meant to delete the source, try REMOVE instead.',
|
||||||
|
self.repo
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
if uris != self.source.uris:
|
||||||
|
self.source.uris = uris
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def add_suite(self, values:str) -> bool:
|
||||||
|
"""Adds a suite to the source"""
|
||||||
|
if not values:
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.log.info('Adding suites: %s', values)
|
||||||
|
suites = self.source.suites
|
||||||
|
|
||||||
|
for suite in values.split():
|
||||||
|
if suite not in suites:
|
||||||
|
suites.append(suite)
|
||||||
|
self.log.debug('Added suite %s', suite)
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.log.warning(
|
||||||
|
'The suite %s was already present in %s',
|
||||||
|
suite,
|
||||||
|
self.repo
|
||||||
|
)
|
||||||
|
|
||||||
|
if suites != self.source.suites:
|
||||||
|
self.source.suites = suites
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def remove_suite(self, values:str) -> bool:
|
||||||
|
"""Remove a suite from the source"""
|
||||||
|
if not values:
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.log.info('Removing suites %s from source %s', values, self.repo)
|
||||||
|
suites = self.source.suites
|
||||||
|
self.log.debug('Starting suites: %s', suites)
|
||||||
|
|
||||||
|
for suite in values.split():
|
||||||
|
try:
|
||||||
|
suites.remove(suite)
|
||||||
|
self.log.debug('Removed suite %s', suite)
|
||||||
|
|
||||||
|
except ValueError:
|
||||||
|
self.log.warning(
|
||||||
|
'The suite %s was not present in %s',
|
||||||
|
suite,
|
||||||
|
self.repo
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(suites) == 0:
|
||||||
|
self.log.error(
|
||||||
|
'Cannot remove the last suite from %s. If you meant to delete the source, try REMOVE instead.',
|
||||||
|
self.repo
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
if suites != self.source.suites:
|
||||||
|
self.source.suites = suites
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def add_component(self, values:str) -> bool:
|
||||||
|
"""Adds components to the source"""
|
||||||
|
if not values:
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.log.info('Adding components: %s', values)
|
||||||
|
components = self.source.components
|
||||||
|
|
||||||
|
for component in values.split():
|
||||||
|
if component not in components:
|
||||||
|
components.append(component)
|
||||||
|
self.log.debug('Added component %s', component)
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.log.warning(
|
||||||
|
'The component %s was already present in %s',
|
||||||
|
component,
|
||||||
|
self.repo
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(components) > 1:
|
||||||
|
if self.source.file.format == util.SourceFormat.LEGACY:
|
||||||
|
self.log.warning(
|
||||||
|
'Adding multiple components to a legacy source is not '
|
||||||
|
'supported. Consider converting the source to DEB822 format.'
|
||||||
|
)
|
||||||
|
|
||||||
|
if components != self.source.components:
|
||||||
|
self.source.components = components
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def remove_component(self, values:str) -> bool:
|
||||||
|
"""Removes components from the source"""
|
||||||
|
if not values:
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.log.info('Removing components %s from source %s', values, self.repo)
|
||||||
|
components = self.source.components
|
||||||
|
self.log.debug('Starting components: %s', components)
|
||||||
|
|
||||||
|
for component in values.split():
|
||||||
|
try:
|
||||||
|
components.remove(component)
|
||||||
|
self.log.debug('Removed component %s', component)
|
||||||
|
|
||||||
|
except ValueError:
|
||||||
|
self.log.warning(
|
||||||
|
'The component %s was not present in %s',
|
||||||
|
component,
|
||||||
|
self.repo
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(components) == 0:
|
||||||
|
self.log.error(
|
||||||
|
'Cannot remove the last component from %s. If you meant to delete the source, try REMOVE instead.',
|
||||||
|
self.repo
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
if components != self.source.components:
|
||||||
|
self.source.components = components
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def add_option(self, values) -> bool:
|
||||||
|
"""TODO: Support options"""
|
||||||
|
raise NotImplementedError(
|
||||||
|
'Options have not been implemented in this version of repolib yet. '
|
||||||
|
f'Please edit the file {self.source.file.path} manually.'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_option(self, values) -> bool:
|
||||||
|
"""TODO: Support options"""
|
||||||
|
raise NotImplementedError(
|
||||||
|
'Options have not been implemented in this version of repolib yet. '
|
||||||
|
f'Please edit the file {self.source.file.path} manually.'
|
||||||
|
)
|
128
repolib/usr/lib/python3/dist-packages/repolib/command/remove.py
Normal file
128
repolib/usr/lib/python3/dist-packages/repolib/command/remove.py
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
"""
|
||||||
|
Copyright (c) 2022, Ian Santopietro
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
This file is part of RepoLib.
|
||||||
|
|
||||||
|
RepoLib is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
RepoLib is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public License
|
||||||
|
along with RepoLib. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .. import util, system
|
||||||
|
from .command import Command
|
||||||
|
|
||||||
|
class Remove(Command):
|
||||||
|
"""Remove subcommand
|
||||||
|
|
||||||
|
Removes configured sources from the system
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--assume-yes, -y
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def init_options(cls, subparsers):
|
||||||
|
"""Sets up the argument parser for this command
|
||||||
|
|
||||||
|
Returns: argparse.subparser
|
||||||
|
This command's subparser
|
||||||
|
"""
|
||||||
|
|
||||||
|
sub = subparsers.add_parser(
|
||||||
|
'remove',
|
||||||
|
help='Remove a configured repository'
|
||||||
|
)
|
||||||
|
|
||||||
|
sub.add_argument(
|
||||||
|
'repository',
|
||||||
|
help='The identifier of the repository to remove. See LIST'
|
||||||
|
)
|
||||||
|
sub.add_argument(
|
||||||
|
'-y',
|
||||||
|
'--assume-yes',
|
||||||
|
action='store_true',
|
||||||
|
help='Remove without prompting for confirmation'
|
||||||
|
)
|
||||||
|
|
||||||
|
def finalize_options(self, args):
|
||||||
|
super().finalize_options(args)
|
||||||
|
system.load_all_sources()
|
||||||
|
self.source_name = args.repository
|
||||||
|
self.assume_yes = args.assume_yes
|
||||||
|
self.source = None
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""Run the command"""
|
||||||
|
|
||||||
|
self.log.info('Looking up %s for removal', self.source_name)
|
||||||
|
|
||||||
|
if self.source_name == 'system':
|
||||||
|
self.log.error('You cannot remove the system sources')
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.source_name not in util.sources:
|
||||||
|
self.log.error(
|
||||||
|
'Source %s was not found. Double-check the spelling',
|
||||||
|
self.source_name
|
||||||
|
)
|
||||||
|
source_list:list = []
|
||||||
|
for source in util.sources:
|
||||||
|
source_list.append(source)
|
||||||
|
suggested_source:str = self.source_name.replace(':', '-')
|
||||||
|
suggested_source = suggested_source.translate(util.CLEAN_CHARS)
|
||||||
|
if not suggested_source in source_list:
|
||||||
|
return False
|
||||||
|
|
||||||
|
response:str = input(f'Did you mean "{suggested_source}"? (Y/n) ')
|
||||||
|
if not response:
|
||||||
|
response = 'y'
|
||||||
|
if response not in util.true_values:
|
||||||
|
return False
|
||||||
|
self.source_name = suggested_source
|
||||||
|
|
||||||
|
self.source = util.sources[self.source_name]
|
||||||
|
self.key = self.source.key
|
||||||
|
self.file = self.source.file
|
||||||
|
|
||||||
|
print(f'This will remove the source {self.source_name}')
|
||||||
|
print(self.source.ui)
|
||||||
|
response:str = 'n'
|
||||||
|
if self.assume_yes:
|
||||||
|
response = 'y'
|
||||||
|
else:
|
||||||
|
response = input('Are you sure you want to do this? (y/N) ')
|
||||||
|
|
||||||
|
if response in util.true_values:
|
||||||
|
self.file.remove_source(self.source_name)
|
||||||
|
self.file.save()
|
||||||
|
|
||||||
|
system.load_all_sources()
|
||||||
|
for source in util.sources.values():
|
||||||
|
self.log.debug('Checking key for %s', source.ident)
|
||||||
|
try:
|
||||||
|
if source.key.path == self.key.path:
|
||||||
|
self.log.info('Source key in use with another source')
|
||||||
|
return True
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.log.info('No other sources found using key, deleting key')
|
||||||
|
if self.key:
|
||||||
|
self.key.delete_key()
|
||||||
|
return True
|
||||||
|
|
||||||
|
else:
|
||||||
|
print('Canceled.')
|
||||||
|
return False
|
544
repolib/usr/lib/python3/dist-packages/repolib/file.py
Normal file
544
repolib/usr/lib/python3/dist-packages/repolib/file.py
Normal file
@ -0,0 +1,544 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
"""
|
||||||
|
Copyright (c) 2022, Ian Santopietro
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
This file is part of RepoLib.
|
||||||
|
|
||||||
|
RepoLib is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
RepoLib is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public License
|
||||||
|
along with RepoLib. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import dbus
|
||||||
|
|
||||||
|
from .source import Source, SourceError
|
||||||
|
from . import util
|
||||||
|
|
||||||
|
FILE_COMMENT = "## Added/managed by repolib ##"
|
||||||
|
|
||||||
|
class SourceFileError(util.RepoError):
|
||||||
|
""" Exception from a source file."""
|
||||||
|
|
||||||
|
def __init__(self, *args, code:int = 1, **kwargs):
|
||||||
|
"""Exception with a source file
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
:code int, optional, default=1: Exception error code.
|
||||||
|
"""
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.code: int = code
|
||||||
|
|
||||||
|
class SourceFile:
|
||||||
|
""" A Source File on disk
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
path(Pathlib.Path): the path for this file on disk
|
||||||
|
name(str): The name for this source (filename less the extension)
|
||||||
|
format(SourceFormat): The format used by this source file
|
||||||
|
contents(list): A list containing all of this file's contents
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, name:str='') -> None:
|
||||||
|
"""Initialize a source file
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
name(str): The filename within the sources directory to load
|
||||||
|
"""
|
||||||
|
self.log = logging.getLogger(__name__)
|
||||||
|
self.name:str = ''
|
||||||
|
self.path:Path = Path()
|
||||||
|
self.alt_path:Path = Path()
|
||||||
|
self.format:util.SourceFormat = util.SourceFormat.DEFAULT
|
||||||
|
self.contents:list = []
|
||||||
|
self.sources:list = []
|
||||||
|
|
||||||
|
self.contents.append(FILE_COMMENT)
|
||||||
|
self.contents.append('#')
|
||||||
|
|
||||||
|
if name:
|
||||||
|
self.name = name
|
||||||
|
self.reset_path()
|
||||||
|
|
||||||
|
if self.path.exists():
|
||||||
|
self.contents = []
|
||||||
|
self.load()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.output
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'SourceFile(name={self.name})'
|
||||||
|
|
||||||
|
def add_source(self, source:Source) -> None:
|
||||||
|
"""Adds a source to the file
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
source(Source): The source to add
|
||||||
|
"""
|
||||||
|
if source not in self.sources:
|
||||||
|
self.contents.append(source)
|
||||||
|
self.sources.append(source)
|
||||||
|
source.file = self
|
||||||
|
|
||||||
|
def remove_source(self, ident:str) -> None:
|
||||||
|
"""Removes a source from the file
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
ident(str): The ident of the source to remove
|
||||||
|
"""
|
||||||
|
source = self.get_source_by_ident(ident)
|
||||||
|
self.contents.remove(source)
|
||||||
|
self.sources.remove(source)
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
## Remove sources prefs files/pin-priority
|
||||||
|
prefs_path = source.prefs
|
||||||
|
try:
|
||||||
|
if prefs_path.exists() and prefs_path.name:
|
||||||
|
prefs_path.unlink()
|
||||||
|
|
||||||
|
except AttributeError:
|
||||||
|
# No prefs path
|
||||||
|
pass
|
||||||
|
|
||||||
|
except PermissionError:
|
||||||
|
bus = dbus.SystemBus()
|
||||||
|
privileged_object = bus.get_object('org.pop_os.repolib', '/Repo')
|
||||||
|
privileged_object.delete_prefs_file(str(prefs_path))
|
||||||
|
|
||||||
|
def get_source_by_ident(self, ident: str) -> Source:
|
||||||
|
"""Find a source within this file by its ident
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
ident(str): The ident to search for
|
||||||
|
|
||||||
|
Returns: Source
|
||||||
|
The located source
|
||||||
|
"""
|
||||||
|
self.log.debug(f'Looking up ident {ident} in {self.name}')
|
||||||
|
for source in self.sources:
|
||||||
|
if source.ident == ident:
|
||||||
|
self.log.debug(f'{ident} found')
|
||||||
|
return source
|
||||||
|
raise SourceFileError(
|
||||||
|
f'The file {self.path} does not contain the source {ident}'
|
||||||
|
)
|
||||||
|
|
||||||
|
def reset_path(self) -> None:
|
||||||
|
"""Attempt to detect the correct path for this File.
|
||||||
|
|
||||||
|
We default to DEB822 .sources format files, but if that file doesn't
|
||||||
|
exist, fallback to legacy .list format. If this also doesn't exist, we
|
||||||
|
swap back to DEB822 format, as this is likely a new file."""
|
||||||
|
self.log.debug('Resetting path')
|
||||||
|
|
||||||
|
default_path = util.SOURCES_DIR / f'{self.name}.sources'
|
||||||
|
legacy_path = util.SOURCES_DIR / f'{self.name}.list'
|
||||||
|
|
||||||
|
if default_path.exists():
|
||||||
|
self.path = default_path
|
||||||
|
self.alt_path = legacy_path
|
||||||
|
self.format = util.SourceFormat.DEFAULT
|
||||||
|
|
||||||
|
elif legacy_path.exists():
|
||||||
|
self.path = legacy_path
|
||||||
|
self.alt_path = default_path
|
||||||
|
self.format = util.SourceFormat.LEGACY
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.path = default_path
|
||||||
|
self.alt_path = legacy_path
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
def find_unique_ident(self, source1:Source, source2:Source) -> bool:
|
||||||
|
"""Takes two sources with identical idents, and finds a new, unique
|
||||||
|
idents for them.
|
||||||
|
|
||||||
|
The rules for this are mildly complicated, and vary depending on the
|
||||||
|
situation:
|
||||||
|
|
||||||
|
* (DEB822) If the sources are identical other than some portion of
|
||||||
|
data, then the two will be combined into a single source.
|
||||||
|
* (legacy) If the two sources are identical other than source type
|
||||||
|
(common with legacy-format PPAs with source code) then the second
|
||||||
|
source will be dropped until export.
|
||||||
|
* (legacy) If the sources differ by URIs, Components, or Suites, then
|
||||||
|
the differing data will be appended to the sources' idents.
|
||||||
|
* (Either) If no other rules can be determined, then the sources will
|
||||||
|
have a number appended to them
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
source1(Source): The original source with the ident
|
||||||
|
source2(Source): The new colliding source with the ident
|
||||||
|
|
||||||
|
Returns: bool
|
||||||
|
`True` if the two sources were successfully deduped, `False` if the
|
||||||
|
second source should be discarded.
|
||||||
|
"""
|
||||||
|
ident_src1:str = source1.ident
|
||||||
|
ident_src2:str = source2.ident
|
||||||
|
|
||||||
|
self.log.debug(f'Idents {ident_src1} and {ident_src2} conflict')
|
||||||
|
|
||||||
|
if self.format == util.SourceFormat.DEFAULT:
|
||||||
|
util.combine_sources(source1, source2)
|
||||||
|
ident_src2 = ''
|
||||||
|
|
||||||
|
else:
|
||||||
|
excl_keys = [
|
||||||
|
'X-Repolib-Name',
|
||||||
|
'X-Repolib-ID',
|
||||||
|
'X-Repolib-Comment',
|
||||||
|
'Enabled',
|
||||||
|
'Types'
|
||||||
|
]
|
||||||
|
if len(source1.types) == 1 and len(source2.types) == 1:
|
||||||
|
if util.compare_sources(source1, source2, excl_keys):
|
||||||
|
util.combine_sources(source1, source2)
|
||||||
|
source1.types = [
|
||||||
|
util.SourceType.BINARY, util.SourceType.SOURCECODE
|
||||||
|
]
|
||||||
|
source1.twin_source = True
|
||||||
|
source1.sourcecode_enabled = source2.enabled
|
||||||
|
ident_src2 = ''
|
||||||
|
diffs = util.find_differences_sources(source1, source2, excl_keys)
|
||||||
|
if diffs:
|
||||||
|
for key in diffs:
|
||||||
|
raw_diffs:tuple = diffs[key]
|
||||||
|
diff1_list = raw_diffs[0].strip().split()
|
||||||
|
diff2_list = raw_diffs[1].strip().split()
|
||||||
|
for i in diff1_list:
|
||||||
|
if i not in diff2_list:
|
||||||
|
ident_src1 += f'-{i}'
|
||||||
|
break
|
||||||
|
for i in diff2_list:
|
||||||
|
if i not in diff1_list:
|
||||||
|
ident_src2 += f'-{i}'
|
||||||
|
break
|
||||||
|
if ident_src1 != ident_src2:
|
||||||
|
break
|
||||||
|
if ident_src2 and ident_src1 != ident_src2:
|
||||||
|
source1.ident = ident_src1
|
||||||
|
source2.ident = ident_src2
|
||||||
|
return True
|
||||||
|
|
||||||
|
elif ident_src2 and ident_src1 == ident_src2:
|
||||||
|
for source in self.sources:
|
||||||
|
src_index = self.sources.index(source)
|
||||||
|
source.ident = f'{self.name}-{src_index}'
|
||||||
|
return True
|
||||||
|
|
||||||
|
elif not ident_src2:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def load(self) -> None:
|
||||||
|
"""Loads the sources from the file on disk"""
|
||||||
|
self.log.debug(f'Loading source file {self.path}')
|
||||||
|
self.contents = []
|
||||||
|
self.sources = []
|
||||||
|
|
||||||
|
if not self.name:
|
||||||
|
raise SourceFileError('You must provide a filename to load.')
|
||||||
|
|
||||||
|
if not self.path.exists():
|
||||||
|
raise SourceFileError(f'The file {self.path} does not exist.')
|
||||||
|
|
||||||
|
with open(self.path, 'r') as source_file:
|
||||||
|
srcfile_data = source_file.readlines()
|
||||||
|
|
||||||
|
item:int = 0
|
||||||
|
raw822:list = []
|
||||||
|
parsing_deb822:bool = False
|
||||||
|
source_name:str = ''
|
||||||
|
commented:bool = False
|
||||||
|
idents:dict = {}
|
||||||
|
|
||||||
|
# Main file parsing loop
|
||||||
|
for line in srcfile_data:
|
||||||
|
comment_found:str = ''
|
||||||
|
name_line:bool = 'X-Repolib-Name' in line
|
||||||
|
|
||||||
|
if not parsing_deb822:
|
||||||
|
commented = line.startswith('#')
|
||||||
|
|
||||||
|
# Find commented out lines
|
||||||
|
if commented:
|
||||||
|
# Exclude disabled legacy deblines
|
||||||
|
valid_legacy = util.validate_debline(line.strip())
|
||||||
|
if not valid_legacy and not name_line:
|
||||||
|
# Found a standard comment
|
||||||
|
self.contents.append(line.strip())
|
||||||
|
|
||||||
|
elif valid_legacy:
|
||||||
|
if self.format != util.SourceFormat.LEGACY:
|
||||||
|
raise SourceFileError(
|
||||||
|
f'File {self.path.name} is an updated file, but '
|
||||||
|
'contains legacy-format sources. This is not '
|
||||||
|
'allowed. Please fix the file manually.'
|
||||||
|
)
|
||||||
|
new_source = Source()
|
||||||
|
new_source.load_from_data([line])
|
||||||
|
if source_name:
|
||||||
|
new_source.name = source_name
|
||||||
|
if not new_source.ident:
|
||||||
|
new_source.ident = self.name
|
||||||
|
to_add:bool = True
|
||||||
|
if new_source.ident in idents:
|
||||||
|
old_source = idents[new_source.ident]
|
||||||
|
idents.pop(old_source.ident)
|
||||||
|
to_add = self.find_unique_ident(old_source, new_source)
|
||||||
|
idents[old_source.ident] = old_source
|
||||||
|
idents[new_source.ident] = new_source
|
||||||
|
if to_add:
|
||||||
|
new_source.file = self
|
||||||
|
self.contents.append(new_source)
|
||||||
|
self.sources.append(new_source)
|
||||||
|
|
||||||
|
elif name_line:
|
||||||
|
source_name = ':'.join(line.split(':')[1:])
|
||||||
|
source_name = source_name.strip()
|
||||||
|
|
||||||
|
# Active legacy line
|
||||||
|
elif not commented:
|
||||||
|
if util.validate_debline(line.strip()):
|
||||||
|
if self.format != util.SourceFormat.LEGACY:
|
||||||
|
raise SourceFileError(
|
||||||
|
f'File {self.path.name} is an updated file, but '
|
||||||
|
'contains legacy-format sources. This is not '
|
||||||
|
'allowed. Please fix the file manually.'
|
||||||
|
)
|
||||||
|
new_source = Source()
|
||||||
|
new_source.load_from_data([line])
|
||||||
|
if source_name:
|
||||||
|
new_source.name = source_name
|
||||||
|
if not new_source.ident:
|
||||||
|
new_source.ident = self.name
|
||||||
|
to_add:bool = True
|
||||||
|
if new_source.ident in idents:
|
||||||
|
old_source = idents[new_source.ident]
|
||||||
|
idents.pop(old_source.ident)
|
||||||
|
to_add = self.find_unique_ident(old_source, new_source)
|
||||||
|
idents[old_source.ident] = old_source
|
||||||
|
idents[new_source.ident] = new_source
|
||||||
|
if to_add:
|
||||||
|
new_source.file = self
|
||||||
|
self.contents.append(new_source)
|
||||||
|
self.sources.append(new_source)
|
||||||
|
|
||||||
|
# Empty lines are treated as comments
|
||||||
|
if line.strip() == '':
|
||||||
|
self.contents.append('')
|
||||||
|
|
||||||
|
# Find 822 sources
|
||||||
|
# Valid sources can begin with any key:
|
||||||
|
for key in util.valid_keys:
|
||||||
|
if line.startswith(key):
|
||||||
|
if self.format == util.SourceFormat.LEGACY:
|
||||||
|
raise SourceFileError(
|
||||||
|
f'File {self.path.name} is a DEB822-format file, but '
|
||||||
|
'contains legacy sources. This is not allowed. '
|
||||||
|
'Please fix the file manually.'
|
||||||
|
)
|
||||||
|
parsing_deb822 = True
|
||||||
|
raw822.append(line.strip())
|
||||||
|
|
||||||
|
item += 1
|
||||||
|
|
||||||
|
elif parsing_deb822:
|
||||||
|
# Deb822 sources are terminated with an empty line
|
||||||
|
if line.strip() == '':
|
||||||
|
parsing_deb822 = False
|
||||||
|
new_source = Source()
|
||||||
|
new_source.load_from_data(raw822)
|
||||||
|
new_source.file = self
|
||||||
|
if source_name:
|
||||||
|
new_source.name = source_name
|
||||||
|
if not new_source.ident:
|
||||||
|
new_source.ident = self.name
|
||||||
|
if new_source.ident in idents:
|
||||||
|
old_source = idents[new_source.ident]
|
||||||
|
idents.pop(old_source.ident)
|
||||||
|
self.find_unique_ident(old_source, new_source)
|
||||||
|
idents[old_source.ident] = old_source
|
||||||
|
idents[new_source.ident] = new_source
|
||||||
|
new_source.file = self
|
||||||
|
self.contents.append(new_source)
|
||||||
|
self.sources.append(new_source)
|
||||||
|
raw822 = []
|
||||||
|
item += 1
|
||||||
|
self.contents.append('')
|
||||||
|
else:
|
||||||
|
raw822.append(line.strip())
|
||||||
|
|
||||||
|
if raw822:
|
||||||
|
parsing_deb822 = False
|
||||||
|
new_source = Source()
|
||||||
|
new_source.load_from_data(raw822)
|
||||||
|
new_source.file = self
|
||||||
|
if source_name:
|
||||||
|
new_source.name = source_name
|
||||||
|
if not new_source.ident:
|
||||||
|
new_source.ident = self.name
|
||||||
|
if new_source.ident in idents:
|
||||||
|
old_source = idents[new_source.ident]
|
||||||
|
idents.pop(old_source.ident)
|
||||||
|
self.find_unique_ident(old_source, new_source)
|
||||||
|
idents[old_source.ident] = old_source
|
||||||
|
idents[new_source.ident] = new_source
|
||||||
|
new_source.file = self
|
||||||
|
self.contents.append(new_source)
|
||||||
|
self.sources.append(new_source)
|
||||||
|
raw822 = []
|
||||||
|
item += 1
|
||||||
|
self.contents.append('')
|
||||||
|
|
||||||
|
for source in self.sources:
|
||||||
|
if not source.has_required_parts:
|
||||||
|
raise SourceFileError(
|
||||||
|
f'The file {self.path.name} is malformed and contains '
|
||||||
|
'errors. Maybe it has some extra new-lines?'
|
||||||
|
)
|
||||||
|
|
||||||
|
self.log.debug('File %s loaded', self.path)
|
||||||
|
|
||||||
|
def save(self) -> None:
|
||||||
|
"""Saves the source file to disk using the current format"""
|
||||||
|
self.log.debug(f'Saving source file to {self.path}')
|
||||||
|
|
||||||
|
for source in self.sources:
|
||||||
|
self.log.debug('New Source %s: \n%s', source.ident, source)
|
||||||
|
|
||||||
|
save_path = util.SOURCES_DIR / f'{self.name}.save'
|
||||||
|
|
||||||
|
for source in self.sources:
|
||||||
|
if source.key:
|
||||||
|
source.key.save_gpg()
|
||||||
|
source.tasks_save()
|
||||||
|
|
||||||
|
if not self.name or not self.format:
|
||||||
|
raise SourceFileError('There was not a complete filename to save')
|
||||||
|
|
||||||
|
if not util.SOURCES_DIR.exists():
|
||||||
|
try:
|
||||||
|
util.SOURCES_DIR.mkdir(parents=True)
|
||||||
|
except PermissionError:
|
||||||
|
self.log.error(
|
||||||
|
'Source destination path does not exist and cannot be created '
|
||||||
|
'Failures expected now.'
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(self.sources) > 0:
|
||||||
|
self.log.debug('Saving, Main path %s; Alt path: %s', self.path, self.alt_path)
|
||||||
|
try:
|
||||||
|
with open(self.path, mode='w') as output_file:
|
||||||
|
output_file.write(self.output)
|
||||||
|
if self.alt_path.exists():
|
||||||
|
self.alt_path.rename(save_path)
|
||||||
|
|
||||||
|
except PermissionError:
|
||||||
|
bus = dbus.SystemBus()
|
||||||
|
privileged_object = bus.get_object('org.pop_os.repolib', '/Repo')
|
||||||
|
privileged_object.output_file_to_disk(self.path.name, self.output)
|
||||||
|
self.log.debug('File %s saved', self.path)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
self.path.unlink(missing_ok=True)
|
||||||
|
self.alt_path.unlink(missing_ok=True)
|
||||||
|
save_path.unlink(missing_ok=True)
|
||||||
|
except PermissionError:
|
||||||
|
bus = dbus.SystemBus()
|
||||||
|
privileged_object = bus.get_object('org.pop_os.repolib', '/Repo')
|
||||||
|
privileged_object.delete_source_file(self.path.name)
|
||||||
|
self.log.debug('File %s removed', self.path)
|
||||||
|
|
||||||
|
|
||||||
|
## Attribute properties
|
||||||
|
@property
|
||||||
|
def format(self) -> util.SourceFormat: # type: ignore (We don't use str.format)
|
||||||
|
"""The format of the file on disk"""
|
||||||
|
return self._format
|
||||||
|
|
||||||
|
@format.setter
|
||||||
|
def format(self, format:util.SourceFormat) -> None: # type: ignore (We don't use str.format)
|
||||||
|
"""The path needs to be updated when the format changes"""
|
||||||
|
alt_format:util.SourceFormat = util.SourceFormat.LEGACY
|
||||||
|
self._format = format
|
||||||
|
self.path = util.SOURCES_DIR / f'{self.name}.{self._format.value}'
|
||||||
|
for format_ in util.SourceFormat:
|
||||||
|
if format != format_:
|
||||||
|
alt_format = format_
|
||||||
|
self.alt_path = util.SOURCES_DIR / f'{self.name}.{alt_format.value}'
|
||||||
|
|
||||||
|
## Output properties
|
||||||
|
@property
|
||||||
|
def legacy(self) -> str:
|
||||||
|
"""Outputs the file in the output_legacy format"""
|
||||||
|
legacy_output:str = ''
|
||||||
|
for item in self.contents:
|
||||||
|
try:
|
||||||
|
legacy_output += item.legacy
|
||||||
|
except AttributeError:
|
||||||
|
legacy_output += item
|
||||||
|
legacy_output += '\n'
|
||||||
|
return legacy_output
|
||||||
|
|
||||||
|
@property
|
||||||
|
def deb822(self) -> str:
|
||||||
|
"""Outputs the file in the output_822 format"""
|
||||||
|
deb822_output:str = ''
|
||||||
|
for item in self.contents:
|
||||||
|
try:
|
||||||
|
deb822_output += item.deb822
|
||||||
|
except AttributeError:
|
||||||
|
deb822_output += item
|
||||||
|
deb822_output += '\n'
|
||||||
|
return deb822_output
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ui(self) -> str:
|
||||||
|
"""Outputs the file in the output_ui format"""
|
||||||
|
ui_output:str = ''
|
||||||
|
for item in self.contents:
|
||||||
|
try:
|
||||||
|
ui_output += item.ui
|
||||||
|
except AttributeError:
|
||||||
|
pass # Skip file comments in UI mode
|
||||||
|
ui_output += '\n'
|
||||||
|
return ui_output
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def output(self) -> str:
|
||||||
|
"""Outputs the file in the output format"""
|
||||||
|
default_output:str = ''
|
||||||
|
for item in self.contents:
|
||||||
|
try:
|
||||||
|
if self.format == util.SourceFormat.DEFAULT:
|
||||||
|
default_output += item.deb822
|
||||||
|
elif self.format == util.SourceFormat.LEGACY:
|
||||||
|
default_output += item.legacy
|
||||||
|
default_output += '\n'
|
||||||
|
except AttributeError:
|
||||||
|
default_output += item
|
||||||
|
default_output += '\n'
|
||||||
|
return default_output
|
||||||
|
|
204
repolib/usr/lib/python3/dist-packages/repolib/key.py
Normal file
204
repolib/usr/lib/python3/dist-packages/repolib/key.py
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
"""
|
||||||
|
Copyright (c) 2022, Ian Santopietro
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
This file is part of RepoLib.
|
||||||
|
|
||||||
|
RepoLib is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
RepoLib is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public License
|
||||||
|
along with RepoLib. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
import dbus
|
||||||
|
import gnupg
|
||||||
|
from pathlib import Path
|
||||||
|
from urllib import request
|
||||||
|
|
||||||
|
from . import util
|
||||||
|
|
||||||
|
SKS_KEYSERVER = 'https://keyserver.ubuntu.com/'
|
||||||
|
SKS_KEYLOOKUP_PATH = 'pks/lookup?op=get&options=mr&exact=on&search=0x'
|
||||||
|
|
||||||
|
class KeyFileError(util.RepoError):
|
||||||
|
""" Exceptions related to apt key files."""
|
||||||
|
|
||||||
|
def __init__(self, *args, code=1, **kwargs):
|
||||||
|
"""Exceptions related to apt key files.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
code (:obj:`int`, optional, default=1): Exception error code.
|
||||||
|
"""
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.code = code
|
||||||
|
|
||||||
|
class SourceKey:
|
||||||
|
"""A signing key for an apt source."""
|
||||||
|
|
||||||
|
def __init__(self, name:str = '') -> None:
|
||||||
|
self.log = logging.getLogger(__name__)
|
||||||
|
self.tmp_path = Path()
|
||||||
|
self.path = Path()
|
||||||
|
self.gpg = gnupg.GPG()
|
||||||
|
self.data = b''
|
||||||
|
|
||||||
|
if name:
|
||||||
|
self.reset_path(name=name)
|
||||||
|
self.setup_gpg()
|
||||||
|
|
||||||
|
def reset_path(self, name: str = '', path:str = '', suffix: str = 'archive-keyring') -> None:
|
||||||
|
"""Set the path for this key
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
suffix(str): The suffix to append to the end of the name to get the
|
||||||
|
file name (default: 'archive-keyring')
|
||||||
|
name(str): The name of the source
|
||||||
|
path(str): The entire path to the key
|
||||||
|
"""
|
||||||
|
self.log.info('Setting path')
|
||||||
|
if not name and not path:
|
||||||
|
raise KeyFileError('A name is required to set the path for this key')
|
||||||
|
|
||||||
|
if name:
|
||||||
|
file_name = f'{name}-{suffix}.gpg'
|
||||||
|
self.tmp_path = util.TEMP_DIR / file_name
|
||||||
|
self.path = util.KEYS_DIR / file_name
|
||||||
|
elif path:
|
||||||
|
self.path = Path(path)
|
||||||
|
self.tmp_path = util.TEMP_DIR / self.path.name
|
||||||
|
|
||||||
|
self.setup_gpg()
|
||||||
|
|
||||||
|
self.log.debug('Key Path: %s', self.path)
|
||||||
|
self.log.debug('Temp Path: %s', self.tmp_path)
|
||||||
|
|
||||||
|
def setup_gpg(self) -> None:
|
||||||
|
"""Set up the GPG object for this key."""
|
||||||
|
self.log.info('Setting up GPG')
|
||||||
|
self.log.debug('Copying %s to %s', self.path, self.tmp_path)
|
||||||
|
try:
|
||||||
|
shutil.copy2(self.path, self.tmp_path)
|
||||||
|
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.gpg = gnupg.GPG(keyring=str(self.tmp_path))
|
||||||
|
self.log.debug('GPG Setup: %s', self.gpg.keyring)
|
||||||
|
|
||||||
|
def save_gpg(self) -> None:
|
||||||
|
"""Saves the key to disk."""
|
||||||
|
self.log.info('Saving key file %s from %s', self.path, self.tmp_path)
|
||||||
|
self.log.debug('Key contents: %s', self.gpg.list_keys())
|
||||||
|
self.log.debug('Temp key exists? %s', self.tmp_path.exists())
|
||||||
|
if not util.KEYS_DIR.exists():
|
||||||
|
try:
|
||||||
|
util.KEYS_DIR.mkdir(parents=True)
|
||||||
|
except PermissionError:
|
||||||
|
self.log.error(
|
||||||
|
'Key destination path does not exist and cannot be created '
|
||||||
|
'Failures expected now.'
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
shutil.copy(self.tmp_path, self.path)
|
||||||
|
|
||||||
|
except PermissionError:
|
||||||
|
bus = dbus.SystemBus()
|
||||||
|
privileged_object = bus.get_object('org.pop_os.repolib', '/Repo')
|
||||||
|
privileged_object.install_signing_key(
|
||||||
|
str(self.tmp_path),
|
||||||
|
str(self.path)
|
||||||
|
)
|
||||||
|
|
||||||
|
def delete_key(self) -> None:
|
||||||
|
"""Deletes the key file from disk."""
|
||||||
|
try:
|
||||||
|
self.tmp_path.unlink()
|
||||||
|
self.path.unlink()
|
||||||
|
|
||||||
|
except PermissionError:
|
||||||
|
bus = dbus.SystemBus()
|
||||||
|
privileged_object = bus.get_object('org.pop_os.repolib', '/Repo')
|
||||||
|
privileged_object.delete_signing_key(str(self.path))
|
||||||
|
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def load_key_data(self, **kwargs) -> None:
|
||||||
|
"""Loads the key data from disk into the object for processing.
|
||||||
|
|
||||||
|
Each of the keyword options specifies one place to look to import key
|
||||||
|
data. Once one is successfully imported, the method returns, so passing
|
||||||
|
multiple won't import multiple keys.
|
||||||
|
|
||||||
|
Keyword Arguments:
|
||||||
|
raw(bytes): Raw data to import to the keyring
|
||||||
|
ascii(str): ASCII-armored key data to import directly
|
||||||
|
url(str): A URL to download key data from
|
||||||
|
fingerprint(str): A key fingerprint to download from `keyserver`
|
||||||
|
keyserver(str): A keyserver to download from.
|
||||||
|
keypath(str): The path on the keyserver from which to download.
|
||||||
|
|
||||||
|
NOTE: The keyserver and keypath args only affect the operation of the
|
||||||
|
`fingerprint` keyword.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self.path.exists():
|
||||||
|
with open(self.path, mode='rb') as keyfile:
|
||||||
|
self.data = keyfile.read()
|
||||||
|
return
|
||||||
|
|
||||||
|
self.tmp_path.touch()
|
||||||
|
|
||||||
|
if 'raw' in kwargs:
|
||||||
|
self.data = kwargs['raw']
|
||||||
|
self.gpg.import_keys(self.data)
|
||||||
|
return
|
||||||
|
|
||||||
|
if 'ascii' in kwargs:
|
||||||
|
self.gpg.import_keys(kwargs['ascii'])
|
||||||
|
if self.tmp_path.exists():
|
||||||
|
with open(self.tmp_path, mode='rb') as keyfile:
|
||||||
|
self.data = keyfile.read()
|
||||||
|
return
|
||||||
|
|
||||||
|
if 'url' in kwargs:
|
||||||
|
req = request.Request(kwargs['url'])
|
||||||
|
with request.urlopen(req) as response:
|
||||||
|
self.data = response.read().decode('UTF-8')
|
||||||
|
self.gpg.import_keys(self.data)
|
||||||
|
return
|
||||||
|
|
||||||
|
if 'fingerprint' in kwargs:
|
||||||
|
if not 'keyserver' in kwargs:
|
||||||
|
kwargs['keyserver'] = SKS_KEYSERVER
|
||||||
|
|
||||||
|
if not 'keypath' in kwargs:
|
||||||
|
kwargs['keypath'] = SKS_KEYLOOKUP_PATH
|
||||||
|
|
||||||
|
key_url = kwargs['keyserver'] + kwargs['keypath'] + kwargs['fingerprint']
|
||||||
|
req = request.Request(key_url)
|
||||||
|
with request.urlopen(req) as response:
|
||||||
|
self.data = response.read().decode('UTF-8')
|
||||||
|
self.gpg.import_keys(self.data)
|
||||||
|
return
|
||||||
|
|
||||||
|
raise TypeError(
|
||||||
|
f'load_key_data() got an unexpected keyword argument "{kwargs.keys()}',
|
||||||
|
' Expected keyword arguments are: [raw, ascii, url, fingerprint]'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
371
repolib/usr/lib/python3/dist-packages/repolib/parsedeb.py
Normal file
371
repolib/usr/lib/python3/dist-packages/repolib/parsedeb.py
Normal file
@ -0,0 +1,371 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
"""
|
||||||
|
Copyright (c) 2022, Ian Santopietro
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
This file is part of RepoLib.
|
||||||
|
|
||||||
|
RepoLib is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
RepoLib is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public License
|
||||||
|
along with RepoLib. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from . import util
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class DebParseError(util.RepoError):
|
||||||
|
""" Exceptions related to parsing deb lines."""
|
||||||
|
|
||||||
|
def __init__(self, *args, code=1, **kwargs):
|
||||||
|
"""Exceptions related to parsing deb lines.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
code (:obj:`int`, optional, default=1): Exception error code.
|
||||||
|
"""
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.code = code
|
||||||
|
|
||||||
|
def debsplit(line:str) -> list:
|
||||||
|
""" Improved string.split() with support for things like [] options.
|
||||||
|
|
||||||
|
Adapted from python-apt
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
line(str): The line to split up.
|
||||||
|
"""
|
||||||
|
line = line.strip()
|
||||||
|
line_list = line.split()
|
||||||
|
for i in line_list:
|
||||||
|
if util.url_validator(i):
|
||||||
|
line_list[line_list.index(i)] = decode_brackets(i)
|
||||||
|
line = ' '.join(line_list)
|
||||||
|
pieces:list = []
|
||||||
|
tmp:str = ""
|
||||||
|
# we are inside a [..] block
|
||||||
|
p_found = False
|
||||||
|
for char in line:
|
||||||
|
if char == '[':
|
||||||
|
p_found = True
|
||||||
|
tmp += char
|
||||||
|
elif char == ']':
|
||||||
|
p_found = False
|
||||||
|
tmp += char
|
||||||
|
elif char.isspace() and not p_found:
|
||||||
|
pieces.append(tmp)
|
||||||
|
tmp = ''
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
tmp += char
|
||||||
|
# append last piece
|
||||||
|
if len(tmp) > 0:
|
||||||
|
pieces.append(tmp)
|
||||||
|
return pieces
|
||||||
|
|
||||||
|
def encode_brackets(word:str) -> str:
|
||||||
|
""" Encodes any [ and ] brackets into URL-safe form
|
||||||
|
|
||||||
|
Technically we should never be recieving these, and there are other things
|
||||||
|
which should technically be encoded as well. However, square brackets
|
||||||
|
actively break the URL parsing, and must be strictly avoided.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
word (str): the string to encode brackets in.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
`str`: the encoded string.
|
||||||
|
"""
|
||||||
|
word = word.replace('[', '%5B')
|
||||||
|
word = word.replace(']', '%5D')
|
||||||
|
return word
|
||||||
|
|
||||||
|
def decode_brackets(word:str) -> str:
|
||||||
|
""" Un-encodes [ and ] from the input
|
||||||
|
|
||||||
|
Since our downstream libraries should also be encoding these correctly, it
|
||||||
|
is better to retain these as the user entered, as that ensures they can
|
||||||
|
recognize it properly.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
word (str): The string to decode.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
`str`: the decoded string.
|
||||||
|
"""
|
||||||
|
word = word.replace('%5B', '[')
|
||||||
|
word = word.replace('%5D', ']')
|
||||||
|
return word
|
||||||
|
|
||||||
|
def parse_name_ident(tail:str) -> tuple:
|
||||||
|
""" Find a Repolib name within the given comment string.
|
||||||
|
|
||||||
|
The name should be headed with "X-Repolib-Name:" and is not space terminated.
|
||||||
|
The ident should be headed with "X-Repolib-ID:" and is space terminated.
|
||||||
|
|
||||||
|
Either field ends at the end of a line, or at a subsequent definition of a
|
||||||
|
different field, or at a subsequent ' #' substring. Additionally, the ident
|
||||||
|
field ends with a subsequent space.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
tail (str): The comment to search within.
|
||||||
|
|
||||||
|
Returns: tuple(name, ident, comment):
|
||||||
|
name (str): The detected name, or None
|
||||||
|
ident (str): The detected ident, or None
|
||||||
|
comment (str): The string with the name and ident removed
|
||||||
|
"""
|
||||||
|
tail = util.strip_hashes(tail)
|
||||||
|
|
||||||
|
# Used for sanity checking later
|
||||||
|
has_name = 'X-Repolib-Name' in tail
|
||||||
|
log.debug('Line name found: %s', has_name)
|
||||||
|
has_ident = 'X-Repolib-ID' in tail
|
||||||
|
log.debug('Line ident found: %s', has_ident)
|
||||||
|
|
||||||
|
parts: list = tail.split()
|
||||||
|
name_found = False
|
||||||
|
ident_found = False
|
||||||
|
name:str = ''
|
||||||
|
ident:str = ''
|
||||||
|
comment:str = ''
|
||||||
|
for item in parts:
|
||||||
|
log.debug("Checking line item: %s", item)
|
||||||
|
item_is_name = item.strip('#').strip().startswith('X-Repolib-Name')
|
||||||
|
item_is_ident = item.strip('#').strip().startswith('X-Repolib-ID')
|
||||||
|
|
||||||
|
if '#' in item and not item_is_name and not item_is_ident:
|
||||||
|
name_found = False
|
||||||
|
ident_found = False
|
||||||
|
|
||||||
|
elif item_is_name:
|
||||||
|
name_found = True
|
||||||
|
ident_found = False
|
||||||
|
continue
|
||||||
|
|
||||||
|
elif item_is_ident:
|
||||||
|
name_found = False
|
||||||
|
ident_found = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
if name_found and not item_is_name:
|
||||||
|
name += f'{item} '
|
||||||
|
continue
|
||||||
|
|
||||||
|
elif ident_found and not item_is_ident:
|
||||||
|
ident += f'{item}'
|
||||||
|
ident_found = False
|
||||||
|
continue
|
||||||
|
|
||||||
|
elif not name_found and not ident_found:
|
||||||
|
c = item.strip('#')
|
||||||
|
comment += f'{c} '
|
||||||
|
|
||||||
|
name = name.strip()
|
||||||
|
ident = ident.strip()
|
||||||
|
comment = comment.strip()
|
||||||
|
|
||||||
|
if not name:
|
||||||
|
if ident:
|
||||||
|
name = ident
|
||||||
|
|
||||||
|
# Final sanity checking
|
||||||
|
if has_name and not name:
|
||||||
|
raise DebParseError(
|
||||||
|
f'Could not parse repository name from comment {comment}. Make sure '
|
||||||
|
'you have a space between the colon and the Name'
|
||||||
|
)
|
||||||
|
if has_ident and not ident:
|
||||||
|
raise DebParseError(
|
||||||
|
f'Could not parse repository ident from comment {comment}. Make sure '
|
||||||
|
'you have a space between the colon and the Ident'
|
||||||
|
)
|
||||||
|
|
||||||
|
return name, ident, comment
|
||||||
|
|
||||||
|
|
||||||
|
class ParseDeb:
|
||||||
|
""" Parsing for source entries.
|
||||||
|
|
||||||
|
Contains parsing helpers for one-line format sources.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, debug:bool = False) -> None:
|
||||||
|
"""
|
||||||
|
Arguments:
|
||||||
|
debug (bool): In debug mode, the structured data is always returned
|
||||||
|
at the end, instead of checking for sanity (default: `False`)
|
||||||
|
"""
|
||||||
|
self.debug = debug
|
||||||
|
self.last_line: str = ''
|
||||||
|
self.last_line_valid: bool = False
|
||||||
|
self.curr_line: str = ''
|
||||||
|
self.curr_line_valid: bool = False
|
||||||
|
|
||||||
|
def parse_options(self, opt:str) -> dict:
|
||||||
|
""" Parses a string of options into a dictionary that repolib can use.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
opt(str): The string with options returned from the line parser.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
`dict`: The dictionary of options with key:val pairs (may be {})
|
||||||
|
"""
|
||||||
|
opt = opt.strip()
|
||||||
|
opt = opt[1:-1].strip() # Remove enclosing brackets
|
||||||
|
options = opt.split()
|
||||||
|
|
||||||
|
parsed_options:dict = {}
|
||||||
|
|
||||||
|
for opt in options:
|
||||||
|
pre_key, values = opt.split('=')
|
||||||
|
values = values.split(',')
|
||||||
|
value:str = ' '.join(values)
|
||||||
|
try:
|
||||||
|
key:str = util.options_inmap[pre_key]
|
||||||
|
except KeyError:
|
||||||
|
raise DebParseError(
|
||||||
|
f'Could not parse line {self.curr_line}: option {opt} is '
|
||||||
|
'not a valid debian repository option or is unsupported.'
|
||||||
|
)
|
||||||
|
parsed_options[key] = value
|
||||||
|
|
||||||
|
return parsed_options
|
||||||
|
|
||||||
|
|
||||||
|
def parse_line(self, line:str) -> dict:
|
||||||
|
""" Parse a deb line into its individual parts.
|
||||||
|
|
||||||
|
Adapted from python-apt
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
line (str): The line input to parse
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(dict): a dict containing the requisite data.
|
||||||
|
"""
|
||||||
|
self.last_line = self.curr_line
|
||||||
|
self.last_line_valid = self.curr_line_valid
|
||||||
|
self.curr_line = line.strip()
|
||||||
|
parts:list = []
|
||||||
|
|
||||||
|
line_is_comment = self.curr_line == '#'
|
||||||
|
line_is_empty = self.curr_line == ''
|
||||||
|
if line_is_comment or line_is_empty:
|
||||||
|
raise DebParseError(f'Current line "{self.curr_line}" is empty')
|
||||||
|
|
||||||
|
line_parsed: dict = {}
|
||||||
|
line_parsed['enabled'] = True
|
||||||
|
line_parsed['name'] = ''
|
||||||
|
line_parsed['ident'] = ''
|
||||||
|
line_parsed['comments'] = []
|
||||||
|
line_parsed['repo_type'] = ''
|
||||||
|
line_parsed['uri'] = ''
|
||||||
|
line_parsed['suite'] = ''
|
||||||
|
line_parsed['components'] = []
|
||||||
|
line_parsed['options'] = {}
|
||||||
|
|
||||||
|
if line.startswith('#'):
|
||||||
|
line_parsed['enabled'] = False
|
||||||
|
line = util.strip_hashes(line)
|
||||||
|
parts = line.split()
|
||||||
|
if not parts[0] in ('deb', 'deb-src'):
|
||||||
|
raise DebParseError(f'Current line "{self.curr_line}" is invalid')
|
||||||
|
|
||||||
|
comments_index = line.find('#')
|
||||||
|
if comments_index > 0:
|
||||||
|
raw_comments:str = line[comments_index + 1:].strip()
|
||||||
|
(
|
||||||
|
line_parsed['name'],
|
||||||
|
line_parsed['ident'],
|
||||||
|
comments
|
||||||
|
) = parse_name_ident(raw_comments)
|
||||||
|
line_parsed['comments'].append(comments)
|
||||||
|
line = line[:comments_index]
|
||||||
|
|
||||||
|
parts = debsplit(line)
|
||||||
|
if len(parts) < 3: # We need at least a type, a URL, and a component
|
||||||
|
raise DebParseError(
|
||||||
|
f'The line "{self.curr_line}" does not have enough pieces to be'
|
||||||
|
'valid'
|
||||||
|
)
|
||||||
|
# Determine the type of the repo
|
||||||
|
repo_type:str = parts.pop(0)
|
||||||
|
if repo_type in ['deb', 'deb-src']:
|
||||||
|
line_parsed['repo_type'] = util.SourceType(repo_type)
|
||||||
|
else:
|
||||||
|
raise DebParseError(f'The line "{self.curr_line}" is of invalid type.')
|
||||||
|
|
||||||
|
# Determine the properties of our repo line
|
||||||
|
uri_index:int = 0
|
||||||
|
is_cdrom: bool = False
|
||||||
|
## The URI index is the vital piece of information we need to parse the
|
||||||
|
## deb line, as it's position determines what other components are
|
||||||
|
## present and where they are. This determines the location of the URI
|
||||||
|
## regardless of where it's at.
|
||||||
|
for part in parts:
|
||||||
|
if part.startswith('['):
|
||||||
|
if 'cdrom' in part:
|
||||||
|
is_cdrom = True
|
||||||
|
uri_index = parts.index(part)
|
||||||
|
else:
|
||||||
|
uri_index = 1
|
||||||
|
|
||||||
|
if is_cdrom:
|
||||||
|
# This could maybe change if the parser now differentiates between
|
||||||
|
# CDROM URIs and option lists
|
||||||
|
raise DebParseError('Repolib cannot currently accept CDROM Sources')
|
||||||
|
|
||||||
|
if uri_index != 0:
|
||||||
|
line_parsed['options'] = self.parse_options(parts.pop(0))
|
||||||
|
|
||||||
|
if len(line_parsed) < 2: # Should have at minimum a URI and a suite/path
|
||||||
|
raise DebParseError(
|
||||||
|
f'The line "{self.curr_line}" does not have enough pieces to be'
|
||||||
|
'valid'
|
||||||
|
)
|
||||||
|
|
||||||
|
line_uri = parts.pop(0)
|
||||||
|
if util.url_validator(line_uri):
|
||||||
|
line_parsed['uri'] = line_uri
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise DebParseError(
|
||||||
|
f'The line "{self.curr_line}" has invalid URI: {line_uri}'
|
||||||
|
)
|
||||||
|
|
||||||
|
line_parsed['suite'] = parts.pop(0)
|
||||||
|
|
||||||
|
line_components:list = []
|
||||||
|
for comp in parts:
|
||||||
|
line_parsed['components'].append(comp)
|
||||||
|
|
||||||
|
|
||||||
|
has_type = line_parsed['repo_type']
|
||||||
|
has_uri = line_parsed['uri']
|
||||||
|
has_suite = line_parsed['suite']
|
||||||
|
|
||||||
|
if has_type and has_uri and has_suite:
|
||||||
|
# if we have these three minimum components, we can proceed and the
|
||||||
|
# line is valid. Otherwise, error out.
|
||||||
|
return line_parsed.copy()
|
||||||
|
|
||||||
|
if self.debug:
|
||||||
|
return line_parsed.copy()
|
||||||
|
|
||||||
|
raise DebParseError(
|
||||||
|
f'The line {self.curr_line} could not be parsed due to an '
|
||||||
|
'unknown error (Probably missing the repo type, URI, or a '
|
||||||
|
'suite/path).'
|
||||||
|
)
|
@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
"""
|
||||||
|
Copyright (c) 2022, Ian Santopietro
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
This file is part of RepoLib.
|
||||||
|
|
||||||
|
RepoLib is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
RepoLib is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public License
|
||||||
|
along with RepoLib. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .popdev import PopdevSource
|
||||||
|
from .ppa import PPASource
|
||||||
|
from ..source import Source
|
||||||
|
|
||||||
|
shortcut_prefixes = {
|
||||||
|
'deb': Source,
|
||||||
|
'deb-src': Source,
|
||||||
|
ppa.prefix: ppa.PPASource,
|
||||||
|
popdev.prefix: popdev.PopdevSource
|
||||||
|
}
|
@ -0,0 +1,171 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
"""
|
||||||
|
Copyright (c) 2022, Ian Santopietro
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
This file is part of RepoLib.
|
||||||
|
|
||||||
|
RepoLib is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
RepoLib is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public License
|
||||||
|
along with RepoLib. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import dbus
|
||||||
|
|
||||||
|
from repolib.key import SourceKey
|
||||||
|
|
||||||
|
from ..source import Source, SourceError
|
||||||
|
from ..file import SourceFile
|
||||||
|
from .. import util
|
||||||
|
|
||||||
|
BASE_FORMAT = util.SourceFormat.DEFAULT
|
||||||
|
BASE_URL = 'http://apt.pop-os.org/staging'
|
||||||
|
BASE_COMPS = 'main'
|
||||||
|
BASE_KEYURL = 'https://raw.githubusercontent.com/pop-os/pop/master/scripts/.iso.asc'
|
||||||
|
|
||||||
|
DEFAULT_FORMAT = util.SourceFormat.DEFAULT
|
||||||
|
|
||||||
|
prefix = 'popdev'
|
||||||
|
delineator = ':'
|
||||||
|
|
||||||
|
class PopdevSource(Source):
|
||||||
|
""" PopDev Source shortcut
|
||||||
|
|
||||||
|
These are given in the format popdev:branchname.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
shortcut (str): The ppa: shortcut to process
|
||||||
|
"""
|
||||||
|
prefs_dir = Path('/etc/apt/preferences.d')
|
||||||
|
default_format = BASE_FORMAT
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validator(shortcut:str) -> bool:
|
||||||
|
"""Determine whether a PPA shortcut is valid.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
shortcut(str): The shortcut to validate
|
||||||
|
|
||||||
|
Returns: bool
|
||||||
|
`True` if the PPA is valid, otherwise False
|
||||||
|
"""
|
||||||
|
if '/' in shortcut:
|
||||||
|
return False
|
||||||
|
|
||||||
|
shortcut_split = shortcut.split(':')
|
||||||
|
try:
|
||||||
|
if not shortcut_split[1]:
|
||||||
|
return False
|
||||||
|
except IndexError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if shortcut.startswith(f'{prefix}:'):
|
||||||
|
shortlist = shortcut.split(':')
|
||||||
|
if len(shortlist) > 0:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __init__(self, *args, line='', fetch_data=True, **kwargs):
|
||||||
|
if line:
|
||||||
|
if not line.startswith('ppa:'):
|
||||||
|
raise SourceError(f'The PPA shortcut {line} is malformed')
|
||||||
|
super().__init__(args, kwargs)
|
||||||
|
self.log = logging.getLogger(__name__)
|
||||||
|
self.line = line
|
||||||
|
self.twin_source:bool = True
|
||||||
|
self.prefs_path = None
|
||||||
|
self.branch_name:str = ''
|
||||||
|
self.branch_url:str = ''
|
||||||
|
if line:
|
||||||
|
self.load_from_shortcut(line)
|
||||||
|
|
||||||
|
def tasks_save(self, *args, **kwargs) -> None:
|
||||||
|
super().tasks_save(*args, **kwargs)
|
||||||
|
self.log.info('Saving prefs file for %s', self.ident)
|
||||||
|
prefs_contents = 'Package: *\n'
|
||||||
|
prefs_contents += f'Pin: release o=pop-os-staging-{self.branch_url}\n'
|
||||||
|
prefs_contents += 'Pin-Priority: 1002\n'
|
||||||
|
|
||||||
|
self.log.debug('%s prefs for pin priority:\n%s', self.ident, prefs_contents)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(self.prefs, mode='w') as prefs_file:
|
||||||
|
prefs_file.write(prefs_contents)
|
||||||
|
except PermissionError:
|
||||||
|
bus = dbus.SystemBus()
|
||||||
|
privileged_object = bus.get_object('org.pop_os.repolib', '/Repo')
|
||||||
|
privileged_object.output_prefs_to_disk(str(self.prefs), prefs_contents)
|
||||||
|
|
||||||
|
self.log.debug('Pin priority saved for %s', self.ident)
|
||||||
|
|
||||||
|
|
||||||
|
def get_description(self) -> str:
|
||||||
|
return f'Pop Development Staging branch'
|
||||||
|
|
||||||
|
|
||||||
|
def load_from_data(self, data: list) -> None:
|
||||||
|
self.log.debug('Loading line %s', data[0])
|
||||||
|
self.load_from_shortcut(shortcut=data[0])
|
||||||
|
|
||||||
|
def load_from_shortcut(self, shortcut:str='', meta:bool=True, get_key:bool=True) -> None:
|
||||||
|
"""Translates the shortcut line into a full repo.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
shortcut(str): The shortcut to load, if one hasn't been loaded yet.
|
||||||
|
"""
|
||||||
|
self.reset_values()
|
||||||
|
if shortcut:
|
||||||
|
self.line = shortcut
|
||||||
|
|
||||||
|
if not self.line:
|
||||||
|
raise SourceError('No PPA shortcut provided')
|
||||||
|
|
||||||
|
if not self.validator(self.line):
|
||||||
|
raise SourceError(f'The line {self.line} is malformed')
|
||||||
|
|
||||||
|
self.log.debug('Loading shortcut %s', self.line)
|
||||||
|
|
||||||
|
self.info_parts = shortcut.split(delineator)
|
||||||
|
self.branch_url = ':'.join(self.info_parts[1:])
|
||||||
|
self.branch_name = util.scrub_filename(name=self.branch_url)
|
||||||
|
self.log.debug('Popdev branch name: %s', self.branch_name)
|
||||||
|
|
||||||
|
self.ident = f'{prefix}-{self.branch_name}'
|
||||||
|
if f'{self.ident}.{BASE_FORMAT.value}' not in util.files:
|
||||||
|
new_file = SourceFile(name=self.ident)
|
||||||
|
new_file.format = BASE_FORMAT
|
||||||
|
self.file = new_file
|
||||||
|
util.files[str(self.file.path)] = self.file
|
||||||
|
else:
|
||||||
|
self.file = util.files[str(self.file.path)]
|
||||||
|
|
||||||
|
self.file.add_source(self)
|
||||||
|
|
||||||
|
self.name = f'Pop Development Branch {self.branch_name}'
|
||||||
|
self.uris = [f'{BASE_URL}/{self.branch_url}']
|
||||||
|
self.suites = [util.DISTRO_CODENAME]
|
||||||
|
self.components = [BASE_COMPS]
|
||||||
|
|
||||||
|
key = SourceKey(name='popdev')
|
||||||
|
key.load_key_data(url=BASE_KEYURL)
|
||||||
|
self.key = key
|
||||||
|
self.signed_by = str(self.key.path)
|
||||||
|
|
||||||
|
self.prefs_path = self.prefs_dir / f'pop-os-staging-{self.branch_name}'
|
||||||
|
self.prefs = self.prefs_path
|
||||||
|
|
||||||
|
self.enabled = True
|
274
repolib/usr/lib/python3/dist-packages/repolib/shortcuts/ppa.py
Normal file
274
repolib/usr/lib/python3/dist-packages/repolib/shortcuts/ppa.py
Normal file
@ -0,0 +1,274 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
"""
|
||||||
|
Copyright (c) 2022, Ian Santopietro
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
This file is part of RepoLib.
|
||||||
|
|
||||||
|
RepoLib is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
RepoLib is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public License
|
||||||
|
along with RepoLib. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from repolib.key import SourceKey
|
||||||
|
|
||||||
|
from ..source import Source, SourceError
|
||||||
|
from ..file import SourceFile
|
||||||
|
from .. import util
|
||||||
|
|
||||||
|
try:
|
||||||
|
from launchpadlib.launchpad import Launchpad
|
||||||
|
from lazr.restfulclient.errors import BadRequest, NotFound, Unauthorized
|
||||||
|
except ImportError:
|
||||||
|
raise SourceError(
|
||||||
|
'Missing optional dependency "launchpadlib". Try `sudo apt install '
|
||||||
|
'python3-launchpadlib` to install it.'
|
||||||
|
)
|
||||||
|
|
||||||
|
BASE_FORMAT = util.SourceFormat.LEGACY
|
||||||
|
BASE_URL = 'http://ppa.launchpad.net'
|
||||||
|
BASE_DIST = 'ubuntu'
|
||||||
|
BASE_COMPS = 'main'
|
||||||
|
|
||||||
|
DEFAULT_FORMAT = util.SourceFormat.LEGACY
|
||||||
|
|
||||||
|
prefix = 'ppa'
|
||||||
|
delineator = ':'
|
||||||
|
|
||||||
|
class PPASource(Source):
|
||||||
|
""" PPA Source shortcut
|
||||||
|
|
||||||
|
These are given in the format ppa:owner/name. Much of this code is adapted
|
||||||
|
from SoftwareProperties.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
shortcut (str): The ppa: shortcut to process
|
||||||
|
fetch_data (bool): Whether to try and fetch metadata from Launchpad.
|
||||||
|
"""
|
||||||
|
|
||||||
|
default_format = BASE_FORMAT
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validator(shortcut:str) -> bool:
|
||||||
|
"""Determine whether a PPA shortcut is valid.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
shortcut(str): The shortcut to validate
|
||||||
|
|
||||||
|
Returns: bool
|
||||||
|
`True` if the PPA is valid, otherwise False
|
||||||
|
"""
|
||||||
|
|
||||||
|
if shortcut.startswith(f'{prefix}:'):
|
||||||
|
shortlist = shortcut.split('/')
|
||||||
|
if len(shortlist) > 1:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __init__(self, *args, line='', fetch_data=True, **kwargs):
|
||||||
|
if line:
|
||||||
|
if not line.startswith('ppa:'):
|
||||||
|
raise SourceError(f'The PPA shortcut {line} is malformed')
|
||||||
|
super().__init__(args, kwargs)
|
||||||
|
self.log = logging.getLogger(__name__)
|
||||||
|
self.line = line
|
||||||
|
self.ppa = None
|
||||||
|
self.twin_source = True
|
||||||
|
self._displayname = ''
|
||||||
|
self._description = ''
|
||||||
|
if line:
|
||||||
|
self.load_from_shortcut(self.line)
|
||||||
|
|
||||||
|
def get_description(self) -> str:
|
||||||
|
output:str = ''
|
||||||
|
output += self.displayname
|
||||||
|
output += '\n\n'
|
||||||
|
output += self.description
|
||||||
|
return output
|
||||||
|
|
||||||
|
def load_from_data(self, data: list) -> None:
|
||||||
|
self.load_from_shortcut(shortcut=data[0])
|
||||||
|
|
||||||
|
def load_from_shortcut(self, shortcut:str='', meta:bool=True, key:bool=True) -> None:
|
||||||
|
"""Translates the shortcut line into a full repo.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
shortcut(str): The shortcut to load, if one hasn't been loaded yet.
|
||||||
|
meta(bool): Whether to fetch repo metadata from Launchpad
|
||||||
|
key(bool): Whether to fetch and install a signing key
|
||||||
|
"""
|
||||||
|
self.reset_values()
|
||||||
|
if shortcut:
|
||||||
|
self.line = shortcut
|
||||||
|
|
||||||
|
if not self.line:
|
||||||
|
raise SourceError('No PPA shortcut provided')
|
||||||
|
|
||||||
|
if not self.validator(self.line):
|
||||||
|
raise SourceError(f'The line {self.line} is malformed')
|
||||||
|
|
||||||
|
line = self.line.replace(prefix + delineator, '')
|
||||||
|
self.info_parts = line.split('/')
|
||||||
|
ppa_owner = self.info_parts[0]
|
||||||
|
ppa_name = self.info_parts[1]
|
||||||
|
|
||||||
|
self.ident = f'{prefix}-{ppa_owner}-{ppa_name}'
|
||||||
|
if f'{self.ident}.{BASE_FORMAT.value}' not in util.files:
|
||||||
|
new_file = SourceFile(name=self.ident)
|
||||||
|
new_file.format = BASE_FORMAT
|
||||||
|
self.file = new_file
|
||||||
|
util.files[str(self.file.path)] = self.file
|
||||||
|
else:
|
||||||
|
self.file = util.files[str(self.file.path)]
|
||||||
|
|
||||||
|
self.file.add_source(self)
|
||||||
|
|
||||||
|
self.name = self.ident
|
||||||
|
self.uris = [f'{BASE_URL}/{ppa_owner}/{ppa_name}/{BASE_DIST}']
|
||||||
|
self.suites = [util.DISTRO_CODENAME]
|
||||||
|
self.components = [BASE_COMPS]
|
||||||
|
|
||||||
|
if meta or key:
|
||||||
|
self.ppa = get_info_from_lp(ppa_owner, ppa_name)
|
||||||
|
self.displayname = self.ppa.displayname
|
||||||
|
self.description = self.ppa.description
|
||||||
|
|
||||||
|
if self.ppa and meta:
|
||||||
|
self.name = self.ppa.displayname
|
||||||
|
|
||||||
|
if self.ppa and key:
|
||||||
|
repo_key = SourceKey(name=self.ident)
|
||||||
|
if str(repo_key.path) not in util.keys:
|
||||||
|
repo_key.load_key_data(fingerprint=self.ppa.fingerprint)
|
||||||
|
util.keys[str(repo_key.path)] = repo_key
|
||||||
|
self.key:SourceKey = repo_key
|
||||||
|
else:
|
||||||
|
self.key = util.keys[repo_key.path]
|
||||||
|
self.signed_by = str(self.key.path)
|
||||||
|
|
||||||
|
self.enabled = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def displayname(self) -> str:
|
||||||
|
"""The name of the PPA provided by launchpad"""
|
||||||
|
if self._displayname:
|
||||||
|
return self._displayname
|
||||||
|
if self.ppa:
|
||||||
|
self._displayname = self.ppa.displayname
|
||||||
|
return self._displayname
|
||||||
|
|
||||||
|
@displayname.setter
|
||||||
|
def displayname(self, displayname) -> None:
|
||||||
|
"""Cache this for use without hitting LP"""
|
||||||
|
self._displayname = displayname
|
||||||
|
|
||||||
|
@property
|
||||||
|
def description(self) -> str:
|
||||||
|
"""The description of the PPA provided by Launchpad"""
|
||||||
|
if self._description:
|
||||||
|
return self._description
|
||||||
|
if self.ppa:
|
||||||
|
self._description = self.ppa.description
|
||||||
|
return self._description
|
||||||
|
|
||||||
|
@description.setter
|
||||||
|
def description(self, desc) -> None:
|
||||||
|
"""Cache this for use without hitting LP"""
|
||||||
|
self._description = desc
|
||||||
|
|
||||||
|
class PPA:
|
||||||
|
""" An object to fetch data from PPAs.
|
||||||
|
|
||||||
|
Portions of this class were adapted from Software Properties
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, teamname, ppaname):
|
||||||
|
self.teamname = teamname
|
||||||
|
self.ppaname = ppaname
|
||||||
|
self._lap = None
|
||||||
|
self._lpteam = None
|
||||||
|
self._lpppa = None
|
||||||
|
self._signing_key_data = None
|
||||||
|
self._fingerprint = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def lap(self):
|
||||||
|
""" The Launchpad Object."""
|
||||||
|
if not self._lap:
|
||||||
|
self._lap = Launchpad.login_anonymously(
|
||||||
|
f'{self.__module__}.{self.__class__.__name__}',
|
||||||
|
service_root='production',
|
||||||
|
version='devel'
|
||||||
|
)
|
||||||
|
return self._lap
|
||||||
|
|
||||||
|
@property
|
||||||
|
def lpteam(self):
|
||||||
|
""" The Launchpad object for the PPA's owner."""
|
||||||
|
if not self._lpteam:
|
||||||
|
try:
|
||||||
|
self._lpteam = self.lap.people(self.teamname) # type: ignore (This won't actually be unbound because of the property)
|
||||||
|
except NotFound as err:
|
||||||
|
msg = f'User/Team "{self.teamname}" not found'
|
||||||
|
raise SourceError(msg) from err
|
||||||
|
except Unauthorized as err:
|
||||||
|
msg = f'Invalid user/team name "{self.teamname}"'
|
||||||
|
raise SourceError(msg) from err
|
||||||
|
return self._lpteam
|
||||||
|
|
||||||
|
@property
|
||||||
|
def lpppa(self):
|
||||||
|
""" The Launchpad object for the PPA."""
|
||||||
|
if not self._lpppa:
|
||||||
|
try:
|
||||||
|
self._lpppa = self.lpteam.getPPAByName(name=self.ppaname)
|
||||||
|
except NotFound as err:
|
||||||
|
msg = f'PPA "{self.teamname}/{self.ppaname}"" not found'
|
||||||
|
raise SourceError(msg) from err
|
||||||
|
except BadRequest as err:
|
||||||
|
msg = f'Invalid PPA name "{self.ppaname}"'
|
||||||
|
raise SourceError(msg) from err
|
||||||
|
return self._lpppa
|
||||||
|
|
||||||
|
@property
|
||||||
|
def description(self) -> str:
|
||||||
|
"""str: The description of the PPA."""
|
||||||
|
return self.lpppa.description or ''
|
||||||
|
|
||||||
|
@property
|
||||||
|
def displayname(self) -> str:
|
||||||
|
""" str: the fancy name of the PPA."""
|
||||||
|
return self.lpppa.displayname or ''
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fingerprint(self):
|
||||||
|
""" str: the fingerprint of the signing key."""
|
||||||
|
if not self._fingerprint:
|
||||||
|
self._fingerprint = self.lpppa.signing_key_fingerprint
|
||||||
|
return self._fingerprint
|
||||||
|
|
||||||
|
|
||||||
|
def get_info_from_lp(owner_name, ppa):
|
||||||
|
""" Attempt to get information on a PPA from launchpad over the internet.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
owner_name (str): The Launchpad user owning the PPA.
|
||||||
|
ppa (str): The name of the PPA
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
json: The PPA information as a JSON object.
|
||||||
|
"""
|
||||||
|
ppa = PPA(owner_name, ppa)
|
||||||
|
return ppa
|
945
repolib/usr/lib/python3/dist-packages/repolib/source.py
Normal file
945
repolib/usr/lib/python3/dist-packages/repolib/source.py
Normal file
@ -0,0 +1,945 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
"""
|
||||||
|
Copyright (c) 2022, Ian Santopietro
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
This file is part of RepoLib.
|
||||||
|
|
||||||
|
RepoLib is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
RepoLib is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public License
|
||||||
|
along with RepoLib. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from debian import deb822
|
||||||
|
|
||||||
|
from .parsedeb import ParseDeb
|
||||||
|
from .key import SourceKey
|
||||||
|
from . import util
|
||||||
|
|
||||||
|
DEFAULT_FORMAT = util.SourceFormat.LEGACY
|
||||||
|
|
||||||
|
class SourceError(util.RepoError):
|
||||||
|
""" Exception from a source object."""
|
||||||
|
|
||||||
|
def __init__(self, *args, code=1, **kwargs):
|
||||||
|
"""Exception with a source object
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
code (:obj:`int`, optional, default=1): Exception error code.
|
||||||
|
"""
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.code = code
|
||||||
|
|
||||||
|
class Source(deb822.Deb822):
|
||||||
|
"""A DEB822 object representing a single software source.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
ident(str): The unique id for this source
|
||||||
|
name(str): The user-readable name for this source
|
||||||
|
enabled(bool): Whether or not the source is enabled
|
||||||
|
types([SourceType]): A list of repository types for this source
|
||||||
|
uris([str]): A list of possible URIs for this source
|
||||||
|
suites([str]): A list of enabled suites for this source
|
||||||
|
components([str]): A list of enabled components for this source
|
||||||
|
comments(str): Comments for this source
|
||||||
|
signed_by(Path): The path to this source's key file
|
||||||
|
file(SourceFile): The file this source belongs to
|
||||||
|
key(SourceKey): The key which signs this source
|
||||||
|
"""
|
||||||
|
|
||||||
|
default_format = DEFAULT_FORMAT
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validator(shortcut:str) -> bool:
|
||||||
|
"""Determine whether a deb line is valid.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
shortcut(str): The shortcut to validate
|
||||||
|
|
||||||
|
Returns: bool
|
||||||
|
`True` if the PPA is valid, otherwise False
|
||||||
|
"""
|
||||||
|
shortcut_list:list = shortcut.split()
|
||||||
|
|
||||||
|
if not shortcut.startswith('deb'):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not len(shortcut_list) > 3:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not util.validate_debline:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if len(shortcut_list) == 3 and '/' not in shortcut_list[-1]:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def __init__(self, *args, file=None, **kwargs) -> None:
|
||||||
|
"""Initialize this source object"""
|
||||||
|
self.log = logging.getLogger(__name__)
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.reset_values()
|
||||||
|
self.file = file
|
||||||
|
self.twin_source = False
|
||||||
|
self.twin_enabled = False
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
"""type: () -> str"""
|
||||||
|
# Append comments to the item
|
||||||
|
# if self.options:
|
||||||
|
|
||||||
|
if self.comments:
|
||||||
|
self['Comments'] = '# '
|
||||||
|
self['Comments'] += ' # '.join(self.comments)
|
||||||
|
|
||||||
|
rep:str = '{%s}' % ', '.join(['%r: %r' % (k, v) for k, v in self.items()])
|
||||||
|
|
||||||
|
rep:str = '{'
|
||||||
|
for key in self:
|
||||||
|
rep += f"{util.PRETTY_PRINT}'{key}': '{self[key]}', "
|
||||||
|
|
||||||
|
rep = rep[:-2]
|
||||||
|
rep += f"{util.PRETTY_PRINT.replace(' ', '')}"
|
||||||
|
rep += '}'
|
||||||
|
|
||||||
|
if self.comments:
|
||||||
|
self.pop('Comments')
|
||||||
|
|
||||||
|
return rep
|
||||||
|
|
||||||
|
def __bool__(self) -> bool:
|
||||||
|
has_uri:bool = len(self.uris) > 0
|
||||||
|
has_suite:bool = len(self.suites) > 0
|
||||||
|
has_component:bool = len(self.components) > 0
|
||||||
|
|
||||||
|
if has_uri and has_suite and has_component:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_description(self) -> str:
|
||||||
|
"""Get a UI-compatible description for a source.
|
||||||
|
|
||||||
|
Returns: (str)
|
||||||
|
The formatted description.
|
||||||
|
"""
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def get_key_info(self, halt_errors: bool = False) -> dict:
|
||||||
|
""" Get a dictionary containing information for the signing key for
|
||||||
|
this source.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
halt_errors (bool): if there are errors or other unexpected data,
|
||||||
|
raise a SourceError exception. Otherwise, simply log a warning.
|
||||||
|
|
||||||
|
Returns: dict
|
||||||
|
The dictionary from gnupg with key info.
|
||||||
|
"""
|
||||||
|
if self.key:
|
||||||
|
keys:list = self.key.gpg.list_keys()
|
||||||
|
if len(keys) > 1:
|
||||||
|
error_msg = (
|
||||||
|
f'The keyring for {self.ident} contains {len(keys)} keys'
|
||||||
|
'. Check the keyring object for details about the keys.'
|
||||||
|
)
|
||||||
|
if halt_errors:
|
||||||
|
raise SourceError(error_msg)
|
||||||
|
self.log.warning(error_msg)
|
||||||
|
return keys[0]
|
||||||
|
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def reset_values(self) -> None:
|
||||||
|
"""Reset the default values for all attributes"""
|
||||||
|
self.log.info('Resetting source info')
|
||||||
|
self.ident = ''
|
||||||
|
self.name = ''
|
||||||
|
self.enabled = True
|
||||||
|
self.types = [util.SourceType.BINARY]
|
||||||
|
self.uris = []
|
||||||
|
self.suites = []
|
||||||
|
self.components = []
|
||||||
|
self.comments = []
|
||||||
|
self.signed_by = None
|
||||||
|
self.architectures = ''
|
||||||
|
self.languages = ''
|
||||||
|
self.targets = ''
|
||||||
|
self.pdiffs = ''
|
||||||
|
self.by_hash = ''
|
||||||
|
self.allow_insecure = ''
|
||||||
|
self.allow_weak = ''
|
||||||
|
self.allow_downgrade_to_insecure = ''
|
||||||
|
self.trusted = ''
|
||||||
|
self.signed_by = ''
|
||||||
|
self.check_valid_until = ''
|
||||||
|
self.valid_until_min = ''
|
||||||
|
self.valid_until_max = ''
|
||||||
|
self.prefs = ''
|
||||||
|
self._update_legacy_options()
|
||||||
|
self.file = None
|
||||||
|
self.key = None
|
||||||
|
|
||||||
|
def load_from_data(self, data:list) -> None:
|
||||||
|
"""Loads source information from the provided data
|
||||||
|
|
||||||
|
Should correctly load either a lecagy Deb line (optionally with
|
||||||
|
preceeding comment) or a DEB822 source.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
data(list): the data to load into the source.
|
||||||
|
"""
|
||||||
|
self.log.info('Loading source from data')
|
||||||
|
self.reset_values()
|
||||||
|
|
||||||
|
if util.validate_debline(data[0]): # Legacy Source
|
||||||
|
if len(data) > 1:
|
||||||
|
raise SourceError(
|
||||||
|
f'The source is a legacy source but contains {len(data)} entries. '
|
||||||
|
'It may only contain one entry.'
|
||||||
|
)
|
||||||
|
deb_parser = ParseDeb()
|
||||||
|
parsed_debline = deb_parser.parse_line(data[0])
|
||||||
|
self.ident = parsed_debline['ident']
|
||||||
|
self.name = parsed_debline['name']
|
||||||
|
self.enabled = parsed_debline['enabled']
|
||||||
|
self.types = [parsed_debline['repo_type']]
|
||||||
|
self.uris = [parsed_debline['uri']]
|
||||||
|
self.suites = [parsed_debline['suite']]
|
||||||
|
self.components = parsed_debline['components']
|
||||||
|
for key in parsed_debline['options']:
|
||||||
|
self[key] = parsed_debline['options'][key]
|
||||||
|
self._update_legacy_options()
|
||||||
|
for comment in parsed_debline['comments']:
|
||||||
|
self.comments.append(comment)
|
||||||
|
if self.comments == ['']:
|
||||||
|
self.comments = []
|
||||||
|
|
||||||
|
if not self.name:
|
||||||
|
self.name = self.generate_default_name()
|
||||||
|
|
||||||
|
if self.signed_by:
|
||||||
|
self.load_key()
|
||||||
|
return
|
||||||
|
|
||||||
|
# DEB822 Source
|
||||||
|
super().__init__(sequence=data)
|
||||||
|
if self.signed_by:
|
||||||
|
self.load_key()
|
||||||
|
return
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sourcecode_enabled(self) -> bool:
|
||||||
|
"""`True` if this source also provides source code, otherwise `False`"""
|
||||||
|
if util.SourceType.SOURCECODE in self.types:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
@sourcecode_enabled.setter
|
||||||
|
def sourcecode_enabled(self, enabled) -> None:
|
||||||
|
"""Accept a variety of input values"""
|
||||||
|
types = [util.SourceType.BINARY]
|
||||||
|
if enabled in util.true_values:
|
||||||
|
types.append(util.SourceType.SOURCECODE)
|
||||||
|
self.types = types
|
||||||
|
|
||||||
|
|
||||||
|
def generate_default_ident(self, prefix='') -> str:
|
||||||
|
"""Generates a suitable ID for the source
|
||||||
|
|
||||||
|
Returns: str
|
||||||
|
A sane default-id
|
||||||
|
"""
|
||||||
|
ident:str = ''
|
||||||
|
if len(self.uris) > 0:
|
||||||
|
uri:str = self.uris[0].replace('/', ' ')
|
||||||
|
uri_list:list = uri.split()
|
||||||
|
uri_str:str = '-'.join(uri_list[1:])
|
||||||
|
branch_name:str = util.scrub_filename(uri_str)
|
||||||
|
ident = f'{prefix}{branch_name}'
|
||||||
|
ident += f'-{self.types[0].ident()}'
|
||||||
|
try:
|
||||||
|
if not self['X-Repolib-ID']:
|
||||||
|
self['X-Repolib-ID'] = ident
|
||||||
|
except KeyError:
|
||||||
|
self['X-Repolib-ID'] = ident
|
||||||
|
return ident
|
||||||
|
|
||||||
|
def generate_default_name(self) -> str:
|
||||||
|
"""Generate a default name based on the ident
|
||||||
|
|
||||||
|
Returns: str
|
||||||
|
A name based on the ident
|
||||||
|
"""
|
||||||
|
name:str = self.ident
|
||||||
|
try:
|
||||||
|
name = self['X-Repolib-Name']
|
||||||
|
except KeyError:
|
||||||
|
self['X-Repolib-Name'] = self.ident
|
||||||
|
return self['X-Repolib-Name']
|
||||||
|
|
||||||
|
if not name:
|
||||||
|
self['X-Repolib-Name'] = self.ident
|
||||||
|
|
||||||
|
return self['X-Repolib-Name']
|
||||||
|
|
||||||
|
def load_key(self, ignore_errors:bool = True) -> None:
|
||||||
|
"""Finds and loads the signing key from the system
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
ignore_errors(bool): If `False`, throw a SourceError exception if
|
||||||
|
the key can't be found/doesn't exist (Default: `True`)
|
||||||
|
"""
|
||||||
|
self.log.info('Finding key for source %s', self.ident)
|
||||||
|
if not self.signed_by:
|
||||||
|
if ignore_errors:
|
||||||
|
self.log.warning('No key configured for source %s', self.ident)
|
||||||
|
else:
|
||||||
|
raise SourceError('No key configured for source {self.ident}')
|
||||||
|
|
||||||
|
if self.signed_by not in util.keys:
|
||||||
|
new_key = SourceKey()
|
||||||
|
new_key.reset_path(path=self.signed_by)
|
||||||
|
self.key = new_key
|
||||||
|
util.keys[str(new_key.path)] = new_key
|
||||||
|
else:
|
||||||
|
self.key = util.keys[self.signed_by]
|
||||||
|
|
||||||
|
def save(self) -> None:
|
||||||
|
"""Proxy method to save the source"""
|
||||||
|
if not self.file == None:
|
||||||
|
self.file.save()
|
||||||
|
|
||||||
|
def output_legacy(self) -> str:
|
||||||
|
"""Outputs a legacy representation of this source
|
||||||
|
|
||||||
|
Note: this is expected to fail if there is more than one type, URI, or
|
||||||
|
Suite; the one-line format does not support having multiple of these
|
||||||
|
properties.
|
||||||
|
|
||||||
|
Returns: str
|
||||||
|
The source output formatted as Legacy
|
||||||
|
"""
|
||||||
|
return self.legacy
|
||||||
|
|
||||||
|
def output_822(self) -> str:
|
||||||
|
"""Outputs a DEB822 representation of this source
|
||||||
|
|
||||||
|
Returns: str
|
||||||
|
The source output formatted as Deb822
|
||||||
|
"""
|
||||||
|
return self.deb822
|
||||||
|
|
||||||
|
def output_ui(self) -> str:
|
||||||
|
"""Outputs a string representation of this source for use in UIs
|
||||||
|
|
||||||
|
Returns: str
|
||||||
|
The source output string
|
||||||
|
"""
|
||||||
|
return self.ui
|
||||||
|
|
||||||
|
def prop_append(self, prop:list, item:str) -> None:
|
||||||
|
"""Appends an item to a list property of this source.
|
||||||
|
NOTE: List properties are `types`, `uris`, `suites`, and `components`.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
prop(list): The property on which to append the item.
|
||||||
|
item(str): The item to append to the propery
|
||||||
|
"""
|
||||||
|
_list = prop
|
||||||
|
_list.append(item)
|
||||||
|
prop = _list
|
||||||
|
|
||||||
|
def tasks_save(self, *args, **kwargs) -> None:
|
||||||
|
"""Extra tasks to perform when saving a source"""
|
||||||
|
return
|
||||||
|
|
||||||
|
## Properties are stored/retrieved from the underlying Deb822 dict
|
||||||
|
@property
|
||||||
|
def has_required_parts(self) -> bool:
|
||||||
|
"""(RO) True if all required attributes are set, otherwise false."""
|
||||||
|
required_parts = ['uris', 'suites', 'ident']
|
||||||
|
|
||||||
|
for attr in required_parts:
|
||||||
|
if len(getattr(self, attr)) < 1:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ident(self) -> str:
|
||||||
|
"""The ident for this source within the file"""
|
||||||
|
try:
|
||||||
|
return self['X-Repolib-ID']
|
||||||
|
except KeyError:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
|
||||||
|
@ident.setter
|
||||||
|
def ident(self, ident: str) -> None:
|
||||||
|
ident = util.scrub_filename(ident)
|
||||||
|
self['X-Repolib-ID'] = ident
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
"""The human-friendly name for this source"""
|
||||||
|
try:
|
||||||
|
_name = self['X-Repolib-Name']
|
||||||
|
except KeyError:
|
||||||
|
_name = ''
|
||||||
|
|
||||||
|
if not _name:
|
||||||
|
self.generate_default_name()
|
||||||
|
return self['X-Repolib-Name']
|
||||||
|
|
||||||
|
@name.setter
|
||||||
|
def name(self, name: str) -> None:
|
||||||
|
self['X-Repolib-Name'] = name
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def enabled(self) -> util.AptSourceEnabled:
|
||||||
|
"""Whether or not the source is enabled/active"""
|
||||||
|
try:
|
||||||
|
enabled = self['Enabled'] in util.true_values
|
||||||
|
except KeyError:
|
||||||
|
return util.AptSourceEnabled.FALSE
|
||||||
|
|
||||||
|
if enabled and self.has_required_parts:
|
||||||
|
return util.AptSourceEnabled.TRUE
|
||||||
|
return util.AptSourceEnabled.FALSE
|
||||||
|
|
||||||
|
@enabled.setter
|
||||||
|
def enabled(self, enabled) -> None:
|
||||||
|
"""For convenience, accept a wide varietry of input value types"""
|
||||||
|
self['Enabled'] = 'no'
|
||||||
|
if enabled in util.true_values:
|
||||||
|
self['Enabled'] = 'yes'
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def types(self) -> list:
|
||||||
|
"""The list of source types for this source"""
|
||||||
|
_types:list = []
|
||||||
|
try:
|
||||||
|
for sourcetype in self['types'].split():
|
||||||
|
_types.append(util.SourceType(sourcetype))
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
return _types
|
||||||
|
|
||||||
|
@types.setter
|
||||||
|
def types(self, types: list) -> None:
|
||||||
|
"""Turn this list into a string of values for storage"""
|
||||||
|
self['Types'] = ''
|
||||||
|
_types:list = []
|
||||||
|
for sourcetype in types:
|
||||||
|
if sourcetype not in _types:
|
||||||
|
_types.append(sourcetype)
|
||||||
|
for sourcetype in _types:
|
||||||
|
self['Types'] += f'{sourcetype.value} '
|
||||||
|
self['Types'] = self['Types'].strip()
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def uris(self) -> list:
|
||||||
|
"""The list of URIs for this source"""
|
||||||
|
try:
|
||||||
|
return self['URIs'].split()
|
||||||
|
except KeyError:
|
||||||
|
return []
|
||||||
|
|
||||||
|
@uris.setter
|
||||||
|
def uris(self, uris: list) -> None:
|
||||||
|
self['URIs'] = ' '.join(uris).strip()
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def suites(self) -> list:
|
||||||
|
"""The list of URIs for this source"""
|
||||||
|
try:
|
||||||
|
return self['Suites'].split()
|
||||||
|
except KeyError:
|
||||||
|
return []
|
||||||
|
|
||||||
|
@suites.setter
|
||||||
|
def suites(self, suites: list) -> None:
|
||||||
|
self['Suites'] = ' '.join(suites).strip()
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def components(self) -> list:
|
||||||
|
"""The list of URIs for this source"""
|
||||||
|
try:
|
||||||
|
return self['Components'].split()
|
||||||
|
except KeyError:
|
||||||
|
return []
|
||||||
|
|
||||||
|
@components.setter
|
||||||
|
def components(self, components: list) -> None:
|
||||||
|
self['Components'] = ' '.join(components).strip()
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def options(self) -> dict:
|
||||||
|
"""The options for this source"""
|
||||||
|
return self._options
|
||||||
|
|
||||||
|
@options.setter
|
||||||
|
def options(self, options:dict) -> None:
|
||||||
|
if 'Signed-By' in options:
|
||||||
|
self.signed_by = options['Signed-By']
|
||||||
|
if self.signed_by:
|
||||||
|
options.pop('Signed-By')
|
||||||
|
self._options = options
|
||||||
|
|
||||||
|
@property
|
||||||
|
def prefs(self):
|
||||||
|
"""The path to any apt preferences files for this source."""
|
||||||
|
try:
|
||||||
|
prefs = self['X-Repolib-Prefs']
|
||||||
|
except KeyError:
|
||||||
|
prefs = ''
|
||||||
|
|
||||||
|
if prefs:
|
||||||
|
return Path(prefs)
|
||||||
|
return Path()
|
||||||
|
|
||||||
|
@prefs.setter
|
||||||
|
def prefs(self, prefs):
|
||||||
|
"""Accept a str or a Path-like object"""
|
||||||
|
try:
|
||||||
|
del self['X-Repolib-Prefs']
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if prefs:
|
||||||
|
prefs_str = str(prefs)
|
||||||
|
self['X-Repolib-Prefs'] = prefs_str
|
||||||
|
|
||||||
|
|
||||||
|
## Option properties
|
||||||
|
|
||||||
|
@property
|
||||||
|
def architectures (self) -> str:
|
||||||
|
"""architectures option"""
|
||||||
|
try:
|
||||||
|
return self['Architectures']
|
||||||
|
except KeyError:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
@architectures.setter
|
||||||
|
def architectures(self, data) -> None:
|
||||||
|
try:
|
||||||
|
self.pop('Architectures')
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if data:
|
||||||
|
self['Architectures'] = data
|
||||||
|
self._update_legacy_options()
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def languages (self) -> str:
|
||||||
|
"""languages option"""
|
||||||
|
try:
|
||||||
|
return self['Languages']
|
||||||
|
except KeyError:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
@languages.setter
|
||||||
|
def languages(self, data) -> None:
|
||||||
|
try:
|
||||||
|
self.pop('Languages')
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if data:
|
||||||
|
self['Languages'] = data
|
||||||
|
self._update_legacy_options()
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def targets (self) -> str:
|
||||||
|
"""targets option"""
|
||||||
|
try:
|
||||||
|
return self['Targets']
|
||||||
|
except KeyError:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
@targets.setter
|
||||||
|
def targets(self, data) -> None:
|
||||||
|
try:
|
||||||
|
self.pop('Targets')
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if data:
|
||||||
|
self['Targets'] = data
|
||||||
|
self._update_legacy_options()
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pdiffs (self) -> str:
|
||||||
|
"""pdiffs option"""
|
||||||
|
try:
|
||||||
|
return self['Pdiffs']
|
||||||
|
except KeyError:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
@pdiffs.setter
|
||||||
|
def pdiffs(self, data) -> None:
|
||||||
|
try:
|
||||||
|
self.pop('Pdiffs')
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if data:
|
||||||
|
self['Pdiffs'] = data
|
||||||
|
self._update_legacy_options()
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def by_hash (self) -> str:
|
||||||
|
"""by_hash option"""
|
||||||
|
try:
|
||||||
|
return self['By-Hash']
|
||||||
|
except KeyError:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
@by_hash.setter
|
||||||
|
def by_hash(self, data) -> None:
|
||||||
|
try:
|
||||||
|
self.pop('By-Hash')
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if data:
|
||||||
|
self['By-Hash'] = data
|
||||||
|
self._update_legacy_options()
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def allow_insecure (self) -> str:
|
||||||
|
"""allow_insecure option"""
|
||||||
|
try:
|
||||||
|
return self['Allow-Insecure']
|
||||||
|
except KeyError:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
@allow_insecure.setter
|
||||||
|
def allow_insecure(self, data) -> None:
|
||||||
|
try:
|
||||||
|
self.pop('Allow-Insecure')
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if data:
|
||||||
|
self['Allow-Insecure'] = data
|
||||||
|
self._update_legacy_options()
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def allow_weak (self) -> str:
|
||||||
|
"""allow_weak option"""
|
||||||
|
try:
|
||||||
|
return self['Allow-Weak']
|
||||||
|
except KeyError:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
@allow_weak.setter
|
||||||
|
def allow_weak(self, data) -> None:
|
||||||
|
try:
|
||||||
|
self.pop('Allow-Weak')
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if data:
|
||||||
|
self['Allow-Weak'] = data
|
||||||
|
self._update_legacy_options()
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def allow_downgrade_to_insecure (self) -> str:
|
||||||
|
"""allow_downgrade_to_insecure option"""
|
||||||
|
try:
|
||||||
|
return self['Allow-Downgrade-To-Insecure']
|
||||||
|
except KeyError:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
@allow_downgrade_to_insecure.setter
|
||||||
|
def allow_downgrade_to_insecure(self, data) -> None:
|
||||||
|
try:
|
||||||
|
self.pop('Allow-Downgrade-To-Insecure')
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if data:
|
||||||
|
self['Allow-Downgrade-To-Insecure'] = data
|
||||||
|
self._update_legacy_options()
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def trusted (self) -> str:
|
||||||
|
"""trusted option"""
|
||||||
|
try:
|
||||||
|
return self['Trusted']
|
||||||
|
except KeyError:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
@trusted.setter
|
||||||
|
def trusted(self, data) -> None:
|
||||||
|
try:
|
||||||
|
self.pop('Trusted')
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if data:
|
||||||
|
self['Trusted'] = data
|
||||||
|
self._update_legacy_options()
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def signed_by (self) -> str:
|
||||||
|
"""signed_by option"""
|
||||||
|
try:
|
||||||
|
return self['Signed-By']
|
||||||
|
except KeyError:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
@signed_by.setter
|
||||||
|
def signed_by(self, data) -> None:
|
||||||
|
try:
|
||||||
|
self.pop('Signed-By')
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if data:
|
||||||
|
self['Signed-By'] = data
|
||||||
|
self._update_legacy_options()
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def check_valid_until (self) -> str:
|
||||||
|
"""check_valid_until option"""
|
||||||
|
try:
|
||||||
|
return self['Check-Valid-Until']
|
||||||
|
except KeyError:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
@check_valid_until.setter
|
||||||
|
def check_valid_until(self, data) -> None:
|
||||||
|
try:
|
||||||
|
self.pop('Check-Valid-Until')
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if data:
|
||||||
|
self['Check-Valid-Until'] = data
|
||||||
|
self._update_legacy_options()
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def valid_until_min (self) -> str:
|
||||||
|
"""valid_until_min option"""
|
||||||
|
try:
|
||||||
|
return self['Valid-Until-Min']
|
||||||
|
except KeyError:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
@valid_until_min.setter
|
||||||
|
def valid_until_min(self, data) -> None:
|
||||||
|
try:
|
||||||
|
self.pop('Valid-Until-Min')
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if data:
|
||||||
|
self['Valid-Until-Min'] = data
|
||||||
|
self._update_legacy_options()
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def valid_until_max (self) -> str:
|
||||||
|
"""valid_until_max option"""
|
||||||
|
try:
|
||||||
|
return self['Valid-Until-Max']
|
||||||
|
except KeyError:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
@valid_until_max.setter
|
||||||
|
def valid_until_max(self, data) -> None:
|
||||||
|
try:
|
||||||
|
self.pop('Valid-Until-Max')
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if data:
|
||||||
|
self['Valid-Until-Max'] = data
|
||||||
|
self._update_legacy_options()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def default_mirror(self) -> str:
|
||||||
|
"""The default mirror/URI for the source"""
|
||||||
|
try:
|
||||||
|
return self['X-Repolib-Default-Mirror']
|
||||||
|
except KeyError:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
@default_mirror.setter
|
||||||
|
def default_mirror(self, mirror) -> None:
|
||||||
|
if mirror:
|
||||||
|
self['X-Repolib-Default-Mirror'] = mirror
|
||||||
|
else:
|
||||||
|
self['X-Repolib-Default-Mirror'] = ''
|
||||||
|
|
||||||
|
|
||||||
|
## Output Properties
|
||||||
|
@property
|
||||||
|
def deb822(self) -> str:
|
||||||
|
"""The DEB822 representation of this source"""
|
||||||
|
self._update_legacy_options()
|
||||||
|
# comments get handled separately because they're a list, and list
|
||||||
|
# properties don't support .append()
|
||||||
|
if self.comments:
|
||||||
|
self['X-Repolib-Comments'] = '# '
|
||||||
|
self['X-Repolib-Comments'] += ' # '.join(self.comments)
|
||||||
|
_deb822 = self.dump()
|
||||||
|
if self.comments:
|
||||||
|
self.pop('X-Repolib-Comments')
|
||||||
|
if _deb822:
|
||||||
|
return _deb822
|
||||||
|
return ''
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ui(self) -> str:
|
||||||
|
"""The UI-friendly representation of this source"""
|
||||||
|
self._update_legacy_options()
|
||||||
|
_ui_list:list = self.deb822.split('\n')
|
||||||
|
ui_output: str = f'{self.ident}:\n'
|
||||||
|
for line in _ui_list:
|
||||||
|
key = line.split(':')[0]
|
||||||
|
if key not in util.output_skip_keys:
|
||||||
|
if line:
|
||||||
|
ui_output += f'{line}\n'
|
||||||
|
for key in util.keys_map:
|
||||||
|
ui_output = ui_output.replace(key, util.keys_map[key])
|
||||||
|
return ui_output
|
||||||
|
|
||||||
|
@property
|
||||||
|
def legacy(self) -> str:
|
||||||
|
"""The legacy/one-line format representation of this source"""
|
||||||
|
self._update_legacy_options()
|
||||||
|
|
||||||
|
if str(self.prefs) != '.':
|
||||||
|
raise SourceError(
|
||||||
|
'Apt Preferences files can only be used with DEB822-format sources.'
|
||||||
|
)
|
||||||
|
|
||||||
|
sourcecode = self.sourcecode_enabled
|
||||||
|
if len(self.types) > 1:
|
||||||
|
self.twin_source = True
|
||||||
|
self.types = [util.SourceType.BINARY]
|
||||||
|
sourcecode = True
|
||||||
|
|
||||||
|
legacy = ''
|
||||||
|
|
||||||
|
legacy += self._generate_legacy_output()
|
||||||
|
if self.twin_source:
|
||||||
|
legacy += '\n'
|
||||||
|
legacy += self._generate_legacy_output(sourcecode=True, enabled=sourcecode)
|
||||||
|
|
||||||
|
return legacy
|
||||||
|
|
||||||
|
def _generate_legacy_output(self, sourcecode=False, enabled=True) -> str:
|
||||||
|
"""Generate a string of the current source in legacy format"""
|
||||||
|
legacy = ''
|
||||||
|
|
||||||
|
if len(self.types) > 1:
|
||||||
|
self.twin_source = True
|
||||||
|
self.types = [util.SourceType.BINARY]
|
||||||
|
for attr in ['types', 'uris', 'suites']:
|
||||||
|
if len(getattr(self, attr)) > 1:
|
||||||
|
msg = f'The source has too many {attr}.'
|
||||||
|
msg += f'Legacy-format sources support one {attr[:-1]} only.'
|
||||||
|
raise SourceError(msg)
|
||||||
|
|
||||||
|
if not self.enabled.get_bool() and not sourcecode:
|
||||||
|
legacy += '# '
|
||||||
|
|
||||||
|
if sourcecode and not enabled:
|
||||||
|
legacy += '# '
|
||||||
|
|
||||||
|
if sourcecode:
|
||||||
|
legacy += 'deb-src '
|
||||||
|
else:
|
||||||
|
legacy += self.types[0].value
|
||||||
|
legacy += ' '
|
||||||
|
|
||||||
|
options_string = self._legacy_options()
|
||||||
|
if options_string:
|
||||||
|
legacy += '['
|
||||||
|
legacy += options_string
|
||||||
|
legacy = legacy.strip()
|
||||||
|
legacy += '] '
|
||||||
|
|
||||||
|
legacy += f'{self.uris[0]} '
|
||||||
|
legacy += f'{self.suites[0]} '
|
||||||
|
|
||||||
|
for component in self.components:
|
||||||
|
legacy += f'{component} '
|
||||||
|
|
||||||
|
legacy += f' ## X-Repolib-Name: {self.name}'
|
||||||
|
legacy += f' # X-Repolib-ID: {self.ident}'
|
||||||
|
if self.comments:
|
||||||
|
for comment in self.comments:
|
||||||
|
legacy += f' # {comment}'
|
||||||
|
|
||||||
|
return legacy
|
||||||
|
|
||||||
|
def _legacy_options(self) -> str:
|
||||||
|
"""Turn the current options into a oneline-style string
|
||||||
|
|
||||||
|
Returns: str
|
||||||
|
The one-line-format options string
|
||||||
|
"""
|
||||||
|
options_str = ''
|
||||||
|
for key in self.options:
|
||||||
|
if self.options[key] != '':
|
||||||
|
options_str += f'{key}={self.options[key].replace(" ", ",")} '
|
||||||
|
return options_str
|
||||||
|
|
||||||
|
def _update_legacy_options(self) -> None:
|
||||||
|
"""Updates the current set of legacy options"""
|
||||||
|
self.options = {
|
||||||
|
'arch': self.architectures,
|
||||||
|
'lang': self.languages,
|
||||||
|
'target': self.targets,
|
||||||
|
'pdiffs': self.pdiffs,
|
||||||
|
'by-hash': self.by_hash,
|
||||||
|
'allow-insecure': self.allow_insecure,
|
||||||
|
'allow-weak': self.allow_weak,
|
||||||
|
'allow-downgrade-to-insecure': self.allow_downgrade_to_insecure,
|
||||||
|
'trusted': self.trusted,
|
||||||
|
'signed-by': self.signed_by,
|
||||||
|
'check-valid-until': self.check_valid_until,
|
||||||
|
'valid-until-min': self.valid_until_min,
|
||||||
|
'valid-until-max': self.valid_until_max,
|
||||||
|
}
|
73
repolib/usr/lib/python3/dist-packages/repolib/system.py
Normal file
73
repolib/usr/lib/python3/dist-packages/repolib/system.py
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
"""
|
||||||
|
Copyright (c) 2022, Ian Santopietro
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
This file is part of RepoLib.
|
||||||
|
|
||||||
|
RepoLib is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
RepoLib is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public License
|
||||||
|
along with RepoLib. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from . import util
|
||||||
|
from .file import SourceFile
|
||||||
|
from .source import Source
|
||||||
|
from .shortcuts import popdev, ppa
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def load_all_sources() -> None:
|
||||||
|
"""Loads all of the sources present on the system."""
|
||||||
|
log.info('Loading all sources')
|
||||||
|
|
||||||
|
util.sources.clear()
|
||||||
|
util.files.clear()
|
||||||
|
util.keys.clear()
|
||||||
|
util.errors.clear()
|
||||||
|
|
||||||
|
sources_path = Path(util.SOURCES_DIR)
|
||||||
|
sources_files = sources_path.glob('*.sources')
|
||||||
|
legacy_files = sources_path.glob('*.list')
|
||||||
|
|
||||||
|
for file in sources_files:
|
||||||
|
try:
|
||||||
|
sourcefile = SourceFile(name=file.stem)
|
||||||
|
log.debug('Loading %s', file)
|
||||||
|
sourcefile.load()
|
||||||
|
if file.name not in util.files:
|
||||||
|
util.files[file.name] = sourcefile
|
||||||
|
|
||||||
|
except Exception as err:
|
||||||
|
util.errors[file.name] = err
|
||||||
|
|
||||||
|
for file in legacy_files:
|
||||||
|
try:
|
||||||
|
sourcefile = SourceFile(name=file.stem)
|
||||||
|
sourcefile.load()
|
||||||
|
util.files[file.name] = sourcefile
|
||||||
|
except Exception as err:
|
||||||
|
util.errors[file.name] = err
|
||||||
|
|
||||||
|
for f in util.files:
|
||||||
|
file = util.files[f]
|
||||||
|
for source in file.sources:
|
||||||
|
if source.ident in util.sources:
|
||||||
|
source.ident = f'{file.name}-{source.ident}'
|
||||||
|
source.file.save()
|
||||||
|
util.sources[source.ident] = source
|
@ -0,0 +1,142 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
"""
|
||||||
|
Copyright (c) 2022, Ian Santopietro
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
This file is part of RepoLib.
|
||||||
|
|
||||||
|
RepoLib is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
RepoLib is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public License
|
||||||
|
along with RepoLib. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from ..key import SourceKey
|
||||||
|
from .. import util, system
|
||||||
|
from .. import set_testing
|
||||||
|
|
||||||
|
# System76 Signing PubKey, for test import
|
||||||
|
KEY_DATA = (
|
||||||
|
'-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBFlL+3MBEADdNM9Xy2t3EtKU1i3R1o'
|
||||||
|
'1OCgJqLiDm8OZZq47InYID8oAPKRjd\n0UDVJTrvfsB4oJH97VRi2hGv2xmc19OaFE/NsQBZW/'
|
||||||
|
'7/3ypLr8eyaNgvscsmG/WN\ncM1cbMZtwd1b0JOr3bNTzp6WKRI3jo9uRw7duM8FwPjKm76Lbo'
|
||||||
|
'DQbAR+4Szm3O8x\n/om8Gs1MRPUkY2dVz5KzednFLHwy7qnUXR3WRB5K1L9EBZkFDDNqnyViUI'
|
||||||
|
'rE4bTm\nBC9mTg/Xfw/QXUFYz3t/YTYduAU0o1q2yei+8tVAJKh7H9t3PrQ95l3RUUcaAvba\n'
|
||||||
|
'A9zlCrI8fonpxu7eSpkqzT4uCkfxdLVwittl1DumKTEkSXDQ5txY21igbSZZQwBA\nZf9MnFhJ'
|
||||||
|
'fPsEIq2YHRc1FBcQxiAIpnGizv7FgYY5FxmZQ7592dMQOZ00h+lDSQug\nNMxloHCogaXR038u'
|
||||||
|
'IKGTQnQEVcT46FtTRkLMSvbigy+RVSchdu9MEBBPgD3vSv53\nNEobXsLiZ9hF6Hk7XI2WxP5j'
|
||||||
|
'1zWTPmzxvf9NDOWz2Sw9Z+ilf252LXoxZQaMngp8\nXL32uvw7q+mjB6F1W/qpe3b32uu7eGNr'
|
||||||
|
'DWJ5veE808hpXXj803TllmRUfMGUrtY9\nk7uUTQQWtrJ5uZ0QmsTk1oJHCPIUjjuiNtQfq28+'
|
||||||
|
'bfg8FEJ/F1N1mB0IvwARAQAB\ntCxQb3AgT1MgKElTTyBTaWduaW5nIEtleSkgPGluZm9Ac3lz'
|
||||||
|
'dGVtNzYuY29tPokC\nNwQTAQIAIgUCWUv7cwIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AA'
|
||||||
|
'CgkQIE3Y\nrsM6ev8kXw/4p/8fOH8wM59ZoU0t1+fv3O8dYaDdTVTVIEno9snrsx5A5tbMu59r'
|
||||||
|
'\nHoBaxGenv/PB0l8yANhRX+HVmU/l0Sj0lTlEkYzgH/IT2Ne60s1ETgI7DlgSuYyP\nH8wq61'
|
||||||
|
'85+2DyE2+R/XcXGq0I++QUq1Y6rS+B4KIyYcgpJotcVNFaZiwuJZE31uLg\nkVMZrm1oObHear'
|
||||||
|
'7P2JQTbgsENMZDJEhQBCGKVdnAfVdKUeUrd07syr0cDe3kwY9o\ncNc00bhIh23cLTJE2omok9'
|
||||||
|
'yCsXoeFJlPMyZw8WvEa5oaYWzP4Yw7nF8/27JTzZ70\nDjK2D2xoTkr0cP87LtZulS6FC3lxLu'
|
||||||
|
'Z6hSaxsqoBH8Dd1uyYVEzLDsIRMtSHsXk+\n3kLrr1p7/7/vjGShlYkbLtP4jWnlHc6vSxIzm/'
|
||||||
|
'MQmQMCfjeo3QH7GGw88mYtXngQ\n/Zna6wz0oL6pGM/4t90SCxTxRqCnoxMxzkcpt9n42bj79g'
|
||||||
|
'rESOMH4wm3ExfuPk7I\nDtY+SqzIq0QvoPbC3XJLusWVgwUsRF2FpTTRTHEiWEMjWDKDVEyT4K'
|
||||||
|
'1k1k3f/gi2\n6LdtXwqDwzUvJJU5HYwVFywt+0jt5F0ZlTlPizz3iHw4gMLOielRShl+gZrU2U'
|
||||||
|
'0O\naj1Hyts9LymEKMUvRQGwMqCZcXo6sGjs59tTsfeGX16PTOyBri8eoLkCDQRZS/tz\nARAA'
|
||||||
|
'pD9IWm4zS1AuBcOTuvh1E/ciKHGIUtW3JftD9ah8loEeckakgG5Xn9he1X6J\nyxPULpsptcCC'
|
||||||
|
'cKXlw853ZQK9PLQJX6moWLH+qf2Zo3UAn/YEsWk+KsHoxPXHNUds\nu/j6UKkqEk8c7H92hUo8'
|
||||||
|
'aWghO3p4HDVJ9KmGtueQ3jOv8Qun7Eh9cIo0A59cKmMv\njKUiYHLIJw8bkveQ8rVPul1ZHn56'
|
||||||
|
'ORiBi58vm3tzjI4UWHQMjiKxXT6H5eG/f5K6\nuaK8lljh6n6jhdnQCpBcdtSIbhE/6YRv2+Ig'
|
||||||
|
'L+BRssvprBtx4/sBwKjNNqzWPeGy\nUDHMiF88ETYqZ8DfukQ/e5XuaxjU41g/F8cw8BeVTBMv'
|
||||||
|
'eb1YTyOoWcWvTL+hoBfS\nqYc/lvDHmmJ7/IgeMvUE6KoByP4ub5wX52mJTqgMC4GMhA04BC60'
|
||||||
|
'B+NfVAXLh2pa\nTRJAHoWTDswOxbR6q9zPEFGZzV04B9Y96EavwMpT5IzG2fOPBwvdT0EDnt+v'
|
||||||
|
'Q/iB\nc9O7CvkRTROAV+RoNCLY2XU8yNc/XxuI66PCE4Q96jW4uDzHvi6sPW/glsfRi2NT\nRW'
|
||||||
|
'CO15KMVf0aypXeBpSbHIXIYGdXRQRpw980IW6PrElPpqZ5/DGbkXei5CuruF2R\nmltuu3MqYQ'
|
||||||
|
'jcUvP9T7s0e5GAFgQFrR/8q29nVULq8IF4vzUAEQEAAYkCHwQYAQIA\nCQUCWUv7cwIbDAAKCR'
|
||||||
|
'AgTdiuwzp6/wTGD/9Co4gEmTTOW++FneMMJo5K4WqeWVRg\ng1q5+yoVqgWq3k6lLsEC5kxR30'
|
||||||
|
'5BAAcvXo9XPKdo62ySYmhIFOpIz/TkeTUxDZaw\nsLtcBxXUME2L5j/1od1V9lxecUvLAgA11o'
|
||||||
|
'5Kb8TMKn5ZcmGhadtTLslWQcYsKqhw\nLaYQRlcxLDHYT8DXFkHgDhUMMbpt07dU5v5lIjgtGN'
|
||||||
|
'HRhdS7/lCmSWOBtYapwpAH\nGYSmahN0zO36VHzOB5uwFue0tSoQiBEvLrCV/8ZtT2S5NkXaSm'
|
||||||
|
'isz6B5Vr6DRtWI\nOamW5pMbSL8WQNQ99Kik05ctERjv2NgxI4JQo/a4KKthRrT4JlixXmrfJD'
|
||||||
|
'uPyDPp\nRuTu1Elo6snoqWKQNf1sEPKvcv7EviNxBOhbTKivWrJXMnbOme7+UlNLcq7VAFp3\n'
|
||||||
|
'x5hxk/ap0WqH/hs7+8jMBC8nS402MoM7EyLS0++kbOuEL/Prf3+JxFRqIu5Df77J\n+bUmTtKI'
|
||||||
|
'CV43ikiVWmnP5OuJj2JPSOTR+rLxAQYpyHmo7HKXE63FbH1FVLgsT88+\nEW6VtI01I7EYmKQX'
|
||||||
|
'EqQo52yfeHKDrQjGNVBWMKcXj0SVU+QQ1Ue/4yLwA+74VD2d\nfOyJI22NfTI+3SMAsMQ8L+WV'
|
||||||
|
'QI+58bu7+iEqoEfHCXikE8BtTbJAN4Oob1lrjfOe\n5utH/lMP9suRWw==\n=NL3f\n-----EN'
|
||||||
|
'D PGP PUBLIC KEY BLOCK-----\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
class KeyTestCase(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
set_testing()
|
||||||
|
self.key_data = KEY_DATA
|
||||||
|
self.keys_dir = util.KEYS_DIR
|
||||||
|
self.key_id = '204DD8AEC33A7AFF'
|
||||||
|
self.key_uids = ['Pop OS (ISO Signing Key) <info@system76.com>']
|
||||||
|
self.key_length = '4096'
|
||||||
|
self.key_date = '1498151795'
|
||||||
|
|
||||||
|
def test_import_ascii(self):
|
||||||
|
key = SourceKey(name='popdev')
|
||||||
|
key.load_key_data(ascii=self.key_data)
|
||||||
|
key_dict = key.gpg.list_keys()[0]
|
||||||
|
key_path = self.keys_dir / 'popdev-archive-keyring.gpg'
|
||||||
|
|
||||||
|
self.assertEqual(key.path, key_path)
|
||||||
|
self.assertEqual(len(key.gpg.list_keys()), 1)
|
||||||
|
self.assertEqual(key_dict['keyid'], self.key_id)
|
||||||
|
self.assertEqual(key_dict['uids'], self.key_uids)
|
||||||
|
self.assertEqual(key_dict['length'], self.key_length)
|
||||||
|
self.assertEqual(key_dict['date'], self.key_date)
|
||||||
|
|
||||||
|
def test_key_save_load(self):
|
||||||
|
print(self.keys_dir)
|
||||||
|
key_path = self.keys_dir / 'popdev-archive-keyring.gpg'
|
||||||
|
if key_path.exists():
|
||||||
|
key_path.unlink()
|
||||||
|
|
||||||
|
self.assertFalse(key_path.exists())
|
||||||
|
key_save = SourceKey(name='popdev')
|
||||||
|
key_save.load_key_data(ascii=self.key_data)
|
||||||
|
key_save.save_gpg()
|
||||||
|
|
||||||
|
self.assertTrue(key_save.path.exists())
|
||||||
|
|
||||||
|
key_load = SourceKey()
|
||||||
|
key_load.reset_path(name='popdev')
|
||||||
|
key_dict = key_load.gpg.list_keys()[0]
|
||||||
|
|
||||||
|
self.assertEqual(key_load.path, key_path)
|
||||||
|
self.assertEqual(len(key_load.gpg.list_keys()), 1)
|
||||||
|
self.assertEqual(key_dict['keyid'], self.key_id)
|
||||||
|
self.assertEqual(key_dict['uids'], self.key_uids)
|
||||||
|
self.assertEqual(key_dict['length'], self.key_length)
|
||||||
|
self.assertEqual(key_dict['date'], self.key_date)
|
||||||
|
|
||||||
|
def test_delete_key(self):
|
||||||
|
key_path = self.keys_dir / 'popdev-archive-keyring.gpg'
|
||||||
|
if key_path.exists():
|
||||||
|
key_path.unlink()
|
||||||
|
|
||||||
|
self.assertFalse(key_path.exists())
|
||||||
|
|
||||||
|
self.assertFalse(key_path.exists())
|
||||||
|
key_save = SourceKey(name='popdev')
|
||||||
|
key_save.load_key_data(ascii=self.key_data)
|
||||||
|
|
||||||
|
key_save.save_gpg()
|
||||||
|
|
||||||
|
self.assertTrue(key_save.path.exists())
|
||||||
|
|
||||||
|
key_load = SourceKey()
|
||||||
|
key_load.reset_path(name='popdev')
|
||||||
|
key_load.delete_key()
|
||||||
|
|
||||||
|
self.assertFalse(key_load.path.exists())
|
@ -0,0 +1,188 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
"""
|
||||||
|
Copyright (c) 2019-2022, Ian Santopietro
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
This file is part of RepoLib.
|
||||||
|
|
||||||
|
RepoLib is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
RepoLib is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public License
|
||||||
|
along with RepoLib. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
This is a library for parsing deb lines into deb822-format data.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from ..source import Source
|
||||||
|
from .. import util
|
||||||
|
|
||||||
|
class DebTestCase(unittest.TestCase):
|
||||||
|
def test_normal_source(self):
|
||||||
|
source = Source()
|
||||||
|
source.load_from_data([
|
||||||
|
'deb http://example.com/ suite main'
|
||||||
|
])
|
||||||
|
source.generate_default_ident()
|
||||||
|
self.assertEqual(source.types, [util.SourceType.BINARY])
|
||||||
|
self.assertTrue(source.enabled.get_bool())
|
||||||
|
self.assertEqual(source.uris, ['http://example.com/'])
|
||||||
|
self.assertEqual(source.suites, ['suite'])
|
||||||
|
self.assertEqual(source.components, ['main'])
|
||||||
|
self.assertEqual(source.ident, 'example-com-binary')
|
||||||
|
|
||||||
|
def test_source_with_multiple_components(self):
|
||||||
|
source = Source()
|
||||||
|
source.load_from_data([
|
||||||
|
'deb http://example.com/ suite main nonfree'
|
||||||
|
])
|
||||||
|
source.generate_default_ident()
|
||||||
|
self.assertEqual(source.suites, ['suite'])
|
||||||
|
self.assertEqual(source.components, ['main', 'nonfree'])
|
||||||
|
|
||||||
|
def test_source_with_option(self):
|
||||||
|
source = Source()
|
||||||
|
source.load_from_data([
|
||||||
|
'deb [ arch=amd64 ] http://example.com/ suite main'
|
||||||
|
])
|
||||||
|
source.generate_default_ident()
|
||||||
|
self.assertEqual(source.uris, ['http://example.com/'])
|
||||||
|
self.assertEqual(source.architectures, 'amd64')
|
||||||
|
|
||||||
|
def test_source_uri_with_brackets(self):
|
||||||
|
source = Source()
|
||||||
|
source.load_from_data([
|
||||||
|
'deb http://example.com/[release]/ubuntu suite main'
|
||||||
|
])
|
||||||
|
source.generate_default_ident()
|
||||||
|
self.assertEqual(source.uris, ['http://example.com/[release]/ubuntu'])
|
||||||
|
|
||||||
|
def test_source_options_with_colons(self):
|
||||||
|
source = Source()
|
||||||
|
source.load_from_data([
|
||||||
|
'deb [ arch=arm:2 ] http://example.com/ suite main'
|
||||||
|
])
|
||||||
|
source.generate_default_ident()
|
||||||
|
self.assertEqual(source.uris, ['http://example.com/'])
|
||||||
|
self.assertEqual(source.architectures, 'arm:2')
|
||||||
|
|
||||||
|
def test_source_with_multiple_option_values(self):
|
||||||
|
source = Source()
|
||||||
|
source.load_from_data([
|
||||||
|
'deb [ arch=armel,amd64 ] http://example.com/ suite main'
|
||||||
|
])
|
||||||
|
source.generate_default_ident()
|
||||||
|
self.assertEqual(source.uris, ['http://example.com/'])
|
||||||
|
self.assertEqual(source.architectures, 'armel amd64')
|
||||||
|
|
||||||
|
def test_source_with_multiple_options(self):
|
||||||
|
source = Source()
|
||||||
|
source.load_from_data([
|
||||||
|
'deb [ arch=amd64 lang=en_US ] http://example.com/ suite main'
|
||||||
|
])
|
||||||
|
source.generate_default_ident()
|
||||||
|
self.assertEqual(source.uris, ['http://example.com/'])
|
||||||
|
self.assertEqual(source.architectures, 'amd64')
|
||||||
|
self.assertEqual(source.languages, 'en_US')
|
||||||
|
|
||||||
|
def test_source_with_multiple_options_with_multiple_values(self):
|
||||||
|
source = Source()
|
||||||
|
source.load_from_data([
|
||||||
|
'deb [ arch=amd64,armel lang=en_US,en_CA ] '
|
||||||
|
'http://example.com/ suite main'
|
||||||
|
])
|
||||||
|
source.generate_default_ident()
|
||||||
|
self.assertEqual(source.uris, ['http://example.com/'])
|
||||||
|
self.assertEqual(source.architectures, 'amd64 armel')
|
||||||
|
self.assertEqual(source.languages, 'en_US en_CA')
|
||||||
|
|
||||||
|
def test_source_uri_with_brackets_and_options(self):
|
||||||
|
source = Source()
|
||||||
|
source.load_from_data([
|
||||||
|
'deb [ arch=amd64 lang=en_US,en_CA ] '
|
||||||
|
'http://example][.com/[release]/ubuntu suite main'
|
||||||
|
])
|
||||||
|
source.generate_default_ident()
|
||||||
|
self.assertEqual(source.uris, ['http://example][.com/[release]/ubuntu'])
|
||||||
|
self.assertEqual(source.architectures, 'amd64')
|
||||||
|
self.assertEqual(source.languages, 'en_US en_CA')
|
||||||
|
|
||||||
|
def test_source_uri_with_brackets_and_options_with_colons(self):
|
||||||
|
source = Source()
|
||||||
|
source.load_from_data([
|
||||||
|
'deb [ arch=amd64,arm:2 lang=en_US,en_CA ] '
|
||||||
|
'http://example][.com/[release]/ubuntu suite main'
|
||||||
|
])
|
||||||
|
source.generate_default_ident()
|
||||||
|
self.assertEqual(source.uris, ['http://example][.com/[release]/ubuntu'])
|
||||||
|
self.assertEqual(source.architectures, 'amd64 arm:2')
|
||||||
|
self.assertEqual(source.languages, 'en_US en_CA')
|
||||||
|
|
||||||
|
def test_worst_case_sourcenario(self):
|
||||||
|
source = Source()
|
||||||
|
source.load_from_data([
|
||||||
|
'deb [ arch=amd64,arm:2,arm][ lang=en_US,en_CA ] '
|
||||||
|
'http://example][.com/[release:good]/ubuntu suite main restricted '
|
||||||
|
'nonfree not-a-component'
|
||||||
|
])
|
||||||
|
source.generate_default_ident()
|
||||||
|
self.assertEqual(source.uris, ['http://example][.com/[release:good]/ubuntu'])
|
||||||
|
self.assertEqual(source.suites, ['suite'])
|
||||||
|
self.assertEqual(source.components, [
|
||||||
|
'main', 'restricted', 'nonfree', 'not-a-component'
|
||||||
|
])
|
||||||
|
source.generate_default_ident()
|
||||||
|
self.assertEqual(source.architectures, 'amd64 arm:2 arm][')
|
||||||
|
self.assertEqual(source.languages, 'en_US en_CA')
|
||||||
|
|
||||||
|
def test_source_code_source(self):
|
||||||
|
source = Source()
|
||||||
|
source.load_from_data([
|
||||||
|
'deb-src http://example.com/ suite main'
|
||||||
|
])
|
||||||
|
source.generate_default_ident()
|
||||||
|
self.assertEqual(source.types, [util.SourceType.SOURCECODE])
|
||||||
|
|
||||||
|
def test_disabled_source(self):
|
||||||
|
source = Source()
|
||||||
|
source.load_from_data([
|
||||||
|
'# deb http://example.com/ suite main'
|
||||||
|
])
|
||||||
|
source.generate_default_ident()
|
||||||
|
self.assertFalse(source.enabled.get_bool())
|
||||||
|
|
||||||
|
def test_disabled_source_without_space(self):
|
||||||
|
source = Source()
|
||||||
|
source.load_from_data([
|
||||||
|
'#deb http://example.com/ suite main'
|
||||||
|
])
|
||||||
|
source.generate_default_ident()
|
||||||
|
self.assertFalse(source.enabled.get_bool())
|
||||||
|
|
||||||
|
def test_source_with_trailing_comment(self):
|
||||||
|
source = Source()
|
||||||
|
source.load_from_data([
|
||||||
|
'deb http://example.com/ suite main # This is a comment'
|
||||||
|
])
|
||||||
|
source.generate_default_ident()
|
||||||
|
self.assertEqual(source.suites, ['suite'])
|
||||||
|
self.assertEqual(source.components, ['main'])
|
||||||
|
|
||||||
|
def test_disabled_source_with_trailing_comment(self):
|
||||||
|
source = Source()
|
||||||
|
source.load_from_data([
|
||||||
|
'# deb http://example.com/ suite main # This is a comment'
|
||||||
|
])
|
||||||
|
source.generate_default_ident()
|
||||||
|
self.assertEqual(source.suites, ['suite'])
|
||||||
|
self.assertEqual(source.components, ['main'])
|
@ -0,0 +1,43 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
"""
|
||||||
|
Copyright (c) 2022, Ian Santopietro
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
This file is part of RepoLib.
|
||||||
|
|
||||||
|
RepoLib is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
RepoLib is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public License
|
||||||
|
along with RepoLib. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from ..shortcuts import popdev
|
||||||
|
from .. import util
|
||||||
|
|
||||||
|
class PopdevTestCase(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_ppa(self):
|
||||||
|
source = popdev.PopdevSource()
|
||||||
|
|
||||||
|
# Verification data
|
||||||
|
uris_test = ['http://apt.pop-os.org/staging/master']
|
||||||
|
signed_test = '/usr/share/keyrings/popdev-archive-keyring.gpg'
|
||||||
|
source.load_from_shortcut(shortcut='popdev:master')
|
||||||
|
|
||||||
|
self.assertEqual(source.uris, uris_test)
|
||||||
|
self.assertEqual(source.ident, 'popdev-master')
|
||||||
|
self.assertEqual(source.suites, [util.DISTRO_CODENAME])
|
||||||
|
self.assertEqual(source.components, ['main'])
|
||||||
|
self.assertEqual(source.types, [util.SourceType.BINARY])
|
||||||
|
self.assertTrue(source.signed_by.endswith(signed_test))
|
@ -0,0 +1,42 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
"""
|
||||||
|
Copyright (c) 2022, Ian Santopietro
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
This file is part of RepoLib.
|
||||||
|
|
||||||
|
RepoLib is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
RepoLib is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public License
|
||||||
|
along with RepoLib. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from ..shortcuts import ppa
|
||||||
|
from .. import util
|
||||||
|
|
||||||
|
class PPATestCase(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_ppa(self):
|
||||||
|
source = ppa.PPASource()
|
||||||
|
|
||||||
|
# Verification data
|
||||||
|
uris_test = ['http://ppa.launchpad.net/system76/pop/ubuntu']
|
||||||
|
signed_test = '/usr/share/keyrings/ppa-system76-pop-archive-keyring.gpg'
|
||||||
|
source.load_from_shortcut(shortcut='ppa:system76/pop', meta=False, key=False)
|
||||||
|
|
||||||
|
self.assertEqual(source.uris, uris_test)
|
||||||
|
self.assertEqual(source.ident, 'ppa-system76-pop')
|
||||||
|
self.assertEqual(source.suites, [util.DISTRO_CODENAME])
|
||||||
|
self.assertEqual(source.components, ['main'])
|
||||||
|
self.assertEqual(source.types, [util.SourceType.BINARY])
|
@ -0,0 +1,245 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
"""
|
||||||
|
Copyright (c) 2022, Ian Santopietro
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
This file is part of RepoLib.
|
||||||
|
|
||||||
|
RepoLib is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
RepoLib is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public License
|
||||||
|
along with RepoLib. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from .. import file, util, source
|
||||||
|
|
||||||
|
class SourceTestCase(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
util.set_testing()
|
||||||
|
self.source = source.Source()
|
||||||
|
self.source.ident = 'test'
|
||||||
|
self.source.name = 'Test Source'
|
||||||
|
self.source.enabled = True
|
||||||
|
self.source.types = [util.SourceType.BINARY, util.SourceType.SOURCECODE]
|
||||||
|
self.source.uris = ['http://example.com/ubuntu', 'http://example.com/mirror']
|
||||||
|
self.source.suites = ['suite', 'suite-updates']
|
||||||
|
self.source.components = ['main', 'contrib', 'nonfree']
|
||||||
|
self.source.architectures = 'amd64 armel'
|
||||||
|
self.source.languages = 'en_US en_CA'
|
||||||
|
self.file = file.SourceFile(name=self.source.ident)
|
||||||
|
self.file.add_source(self.source)
|
||||||
|
self.source.file = self.file
|
||||||
|
|
||||||
|
self.source_legacy = source.Source()
|
||||||
|
self.source_legacy.ident = 'test-legacy'
|
||||||
|
self.source_legacy.name = 'Test Legacy Source'
|
||||||
|
self.source_legacy.enabled = True
|
||||||
|
self.source_legacy.types = [util.SourceType.BINARY]
|
||||||
|
self.source_legacy.uris = ['http://example.com/ubuntu']
|
||||||
|
self.source_legacy.suites = ['suite']
|
||||||
|
self.source_legacy.components = ['main', 'contrib', 'nonfree']
|
||||||
|
self.source_legacy.architectures = 'amd64 armel'
|
||||||
|
self.source_legacy.languages = 'en_US en_CA'
|
||||||
|
self.source_legacy.file = file.SourceFile(name=self.source_legacy.ident)
|
||||||
|
self.source_legacy.file.format = util.SourceFormat.LEGACY
|
||||||
|
|
||||||
|
|
||||||
|
def test_default_source_data(self):
|
||||||
|
self.assertEqual(self.source.name, 'Test Source')
|
||||||
|
self.assertTrue(self.source.enabled.get_bool())
|
||||||
|
self.assertEqual(
|
||||||
|
self.source.types,
|
||||||
|
[util.SourceType.BINARY, util.SourceType.SOURCECODE]
|
||||||
|
)
|
||||||
|
self.assertTrue(self.source.sourcecode_enabled)
|
||||||
|
self.assertEqual(
|
||||||
|
self.source.uris,
|
||||||
|
['http://example.com/ubuntu', 'http://example.com/mirror']
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.source.suites,
|
||||||
|
['suite', 'suite-updates']
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.source.components,
|
||||||
|
['main', 'contrib', 'nonfree']
|
||||||
|
)
|
||||||
|
self.assertEqual(self.source.architectures, 'amd64 armel')
|
||||||
|
self.assertEqual(self.source.languages, 'en_US en_CA')
|
||||||
|
self.assertEqual(self.source.file.path.name, 'test.sources')
|
||||||
|
|
||||||
|
def test_output_822(self):
|
||||||
|
source_string = (
|
||||||
|
'X-Repolib-ID: test\n'
|
||||||
|
'X-Repolib-Name: Test Source\n'
|
||||||
|
'Enabled: yes\n'
|
||||||
|
'Types: deb deb-src\n'
|
||||||
|
'URIs: http://example.com/ubuntu http://example.com/mirror\n'
|
||||||
|
'Suites: suite suite-updates\n'
|
||||||
|
'Components: main contrib nonfree\n'
|
||||||
|
'Architectures: amd64 armel\n'
|
||||||
|
'Languages: en_US en_CA\n'
|
||||||
|
)
|
||||||
|
legacy_source_string = (
|
||||||
|
'X-Repolib-ID: test-legacy\n'
|
||||||
|
'X-Repolib-Name: Test Legacy Source\n'
|
||||||
|
'Enabled: yes\n'
|
||||||
|
'Types: deb\n'
|
||||||
|
'URIs: http://example.com/ubuntu\n'
|
||||||
|
'Suites: suite\n'
|
||||||
|
'Components: main contrib nonfree\n'
|
||||||
|
'Architectures: amd64 armel\n'
|
||||||
|
'Languages: en_US en_CA\n'
|
||||||
|
)
|
||||||
|
self.assertEqual(self.source.deb822, source_string)
|
||||||
|
self.assertEqual(self.source_legacy.deb822, legacy_source_string)
|
||||||
|
|
||||||
|
def test_output_ui(self):
|
||||||
|
source_string = (
|
||||||
|
'test:\n'
|
||||||
|
'Name: Test Source\n'
|
||||||
|
'Enabled: yes\n'
|
||||||
|
'Types: deb deb-src\n'
|
||||||
|
'URIs: http://example.com/ubuntu http://example.com/mirror\n'
|
||||||
|
'Suites: suite suite-updates\n'
|
||||||
|
'Components: main contrib nonfree\n'
|
||||||
|
'Architectures: amd64 armel\n'
|
||||||
|
'Languages: en_US en_CA\n'
|
||||||
|
''
|
||||||
|
)
|
||||||
|
legacy_source_string = (
|
||||||
|
'test-legacy:\n'
|
||||||
|
'Name: Test Legacy Source\n'
|
||||||
|
'Enabled: yes\n'
|
||||||
|
'Types: deb\n'
|
||||||
|
'URIs: http://example.com/ubuntu\n'
|
||||||
|
'Suites: suite\n'
|
||||||
|
'Components: main contrib nonfree\n'
|
||||||
|
'Architectures: amd64 armel\n'
|
||||||
|
'Languages: en_US en_CA\n'
|
||||||
|
)
|
||||||
|
self.assertEqual(self.source.ui, source_string)
|
||||||
|
self.assertEqual(self.source_legacy.ui, legacy_source_string)
|
||||||
|
|
||||||
|
def test_output_legacy(self):
|
||||||
|
source_string = (
|
||||||
|
'deb [arch=amd64,armel lang=en_US,en_CA] http://example.com/ubuntu suite main contrib nonfree ## X-Repolib-Name: Test Legacy Source # X-Repolib-ID: test-legacy'
|
||||||
|
)
|
||||||
|
self.assertEqual(self.source_legacy.legacy, source_string)
|
||||||
|
|
||||||
|
def test_enabled(self):
|
||||||
|
self.source.enabled = False
|
||||||
|
self.assertFalse(self.source.enabled.get_bool())
|
||||||
|
|
||||||
|
def test_sourcecode_enabled(self):
|
||||||
|
self.source.sourcecode_enabled = False
|
||||||
|
self.assertEqual(self.source.types, [util.SourceType.BINARY])
|
||||||
|
|
||||||
|
def test_dict_access(self):
|
||||||
|
self.assertEqual(self.source['X-Repolib-ID'], 'test')
|
||||||
|
self.assertEqual(self.source['X-Repolib-Name'], 'Test Source')
|
||||||
|
self.assertEqual(self.source['Enabled'], 'yes')
|
||||||
|
self.assertEqual(self.source['Enabled'], 'yes')
|
||||||
|
self.assertEqual(self.source['Types'], 'deb deb-src')
|
||||||
|
self.assertEqual(self.source['URIs'], 'http://example.com/ubuntu http://example.com/mirror')
|
||||||
|
self.assertEqual(self.source['Suites'], 'suite suite-updates')
|
||||||
|
self.assertEqual(self.source['Components'], 'main contrib nonfree')
|
||||||
|
self.assertEqual(self.source['Architectures'], 'amd64 armel')
|
||||||
|
self.assertEqual(self.source['Languages'], 'en_US en_CA')
|
||||||
|
|
||||||
|
def test_load(self):
|
||||||
|
load_source = source.Source()
|
||||||
|
load_source.load_from_data([
|
||||||
|
'X-Repolib-ID: load-test',
|
||||||
|
'X-Repolib-Name: Test Source Loading',
|
||||||
|
'Enabled: yes',
|
||||||
|
'Types: deb',
|
||||||
|
'URIs: http://example.com/ubuntu http://example.com/mirror',
|
||||||
|
'Suites: suite suite-updates',
|
||||||
|
'Components: main contrib nonfree',
|
||||||
|
'Architectures: amd64 armel',
|
||||||
|
'Languages: en_US en_CA',
|
||||||
|
])
|
||||||
|
|
||||||
|
self.assertEqual(load_source.ident, 'load-test')
|
||||||
|
self.assertEqual(load_source.name, 'Test Source Loading')
|
||||||
|
self.assertTrue(load_source.enabled.get_bool())
|
||||||
|
self.assertEqual(
|
||||||
|
load_source.types,
|
||||||
|
[util.SourceType.BINARY]
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
load_source.uris,
|
||||||
|
['http://example.com/ubuntu', 'http://example.com/mirror']
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
load_source.suites,
|
||||||
|
['suite', 'suite-updates']
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
load_source.components,
|
||||||
|
['main', 'contrib', 'nonfree']
|
||||||
|
)
|
||||||
|
self.assertEqual(load_source.architectures, 'amd64 armel')
|
||||||
|
self.assertEqual(load_source.languages, 'en_US en_CA')
|
||||||
|
|
||||||
|
load_legacy_source = source.Source()
|
||||||
|
load_legacy_source.load_from_data(
|
||||||
|
['deb [arch=amd64,armel lang=en_US,en_CA] http://example.com/ubuntu suite main contrib nonfree ## X-Repolib-Name: Test Legacy Source Loading # X-Repolib-ID: test-load-legacy']
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(load_legacy_source.ident, 'test-load-legacy')
|
||||||
|
self.assertEqual(load_legacy_source.name, 'Test Legacy Source Loading')
|
||||||
|
self.assertTrue(load_legacy_source.enabled.get_bool())
|
||||||
|
self.assertEqual(
|
||||||
|
load_legacy_source.types,
|
||||||
|
[util.SourceType.BINARY]
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
load_legacy_source.uris,
|
||||||
|
['http://example.com/ubuntu']
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
load_legacy_source.suites,
|
||||||
|
['suite']
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
load_legacy_source.components,
|
||||||
|
['main', 'contrib', 'nonfree']
|
||||||
|
)
|
||||||
|
self.assertEqual(load_legacy_source.architectures, 'amd64 armel')
|
||||||
|
self.assertEqual(load_legacy_source.languages, 'en_US en_CA')
|
||||||
|
|
||||||
|
def test_save_load(self):
|
||||||
|
self.source.file.save()
|
||||||
|
load_source_file = file.SourceFile(name='test')
|
||||||
|
load_source_file.load()
|
||||||
|
self.assertGreater(len(load_source_file.sources), 0)
|
||||||
|
self.assertGreater(
|
||||||
|
len(load_source_file.contents), len(load_source_file.sources)
|
||||||
|
)
|
||||||
|
load_source = load_source_file.sources[0]
|
||||||
|
|
||||||
|
self.assertEqual(load_source.ident, self.source.ident)
|
||||||
|
self.assertEqual(load_source.name, self.source.name)
|
||||||
|
self.assertEqual(load_source.enabled, self.source.enabled)
|
||||||
|
self.assertEqual(load_source.types, self.source.types)
|
||||||
|
self.assertEqual(load_source.sourcecode_enabled, self.source.sourcecode_enabled)
|
||||||
|
self.assertEqual(load_source.uris, self.source.uris)
|
||||||
|
self.assertEqual(load_source.suites, self.source.suites)
|
||||||
|
self.assertEqual(load_source.components, self.source.components)
|
||||||
|
self.assertEqual(load_source.architectures, self.source.architectures)
|
||||||
|
self.assertEqual(load_source.languages, self.source.languages)
|
||||||
|
self.assertEqual(load_source.file.name, self.source.file.name)
|
||||||
|
|
455
repolib/usr/lib/python3/dist-packages/repolib/util.py
Normal file
455
repolib/usr/lib/python3/dist-packages/repolib/util.py
Normal file
@ -0,0 +1,455 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
"""
|
||||||
|
Copyright (c) 2019-2022, Ian Santopietro
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
This file is part of RepoLib.
|
||||||
|
|
||||||
|
RepoLib is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
RepoLib is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public License
|
||||||
|
along with RepoLib. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import atexit
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
from enum import Enum
|
||||||
|
from pathlib import Path
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
from urllib import request, error
|
||||||
|
|
||||||
|
import dbus
|
||||||
|
|
||||||
|
SOURCES_DIR = Path('/etc/apt/sources.list.d')
|
||||||
|
KEYS_DIR = Path('/etc/apt/keyrings/')
|
||||||
|
TESTING = False
|
||||||
|
KEYSERVER_QUERY_URL = 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x'
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class RepoError(Exception):
|
||||||
|
""" Exception from this module."""
|
||||||
|
|
||||||
|
def __init__(self, *args, code=1, **kwargs):
|
||||||
|
"""Exception with a source object
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
code (:obj:`int`, optional, default=1): Exception error code.
|
||||||
|
"""
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.code = code
|
||||||
|
|
||||||
|
try:
|
||||||
|
import distro
|
||||||
|
DISTRO_CODENAME = distro.codename()
|
||||||
|
except ImportError:
|
||||||
|
DISTRO_CODENAME = 'linux'
|
||||||
|
|
||||||
|
class SourceFormat(Enum):
|
||||||
|
"""Enum of SourceFile Formats"""
|
||||||
|
DEFAULT = "sources"
|
||||||
|
LEGACY = "list"
|
||||||
|
|
||||||
|
class SourceType(Enum):
|
||||||
|
"""Enum of repository types"""
|
||||||
|
BINARY = 'deb'
|
||||||
|
SOURCECODE = 'deb-src'
|
||||||
|
|
||||||
|
def ident(self) -> str:
|
||||||
|
"""Used for getting a version of the format for idents"""
|
||||||
|
ident = f'{self.value}'
|
||||||
|
ident = ident.replace('deb-src', 'source')
|
||||||
|
ident = ident.replace('deb', 'binary')
|
||||||
|
return ident
|
||||||
|
|
||||||
|
class AptSourceEnabled(Enum):
|
||||||
|
""" Helper Enum to translate between bool data and the Deb822 format. """
|
||||||
|
TRUE = 'yes'
|
||||||
|
FALSE = 'no'
|
||||||
|
|
||||||
|
def get_bool(self):
|
||||||
|
""" Return a bool based on the value. """
|
||||||
|
# pylint: disable=comparison-with-callable
|
||||||
|
# This doesnt seem to actually be a callable in this case.
|
||||||
|
if self.value == "yes":
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
valid_keys = [
|
||||||
|
'X-Repolib-Name:',
|
||||||
|
'X-Repolib-ID:',
|
||||||
|
'X-Repolib-Default-Mirror:',
|
||||||
|
'X-Repolib-Comment',
|
||||||
|
'X-Repolib-Prefs',
|
||||||
|
'Enabled:',
|
||||||
|
'Types:',
|
||||||
|
'URIs:',
|
||||||
|
'Suites:',
|
||||||
|
'Components:',
|
||||||
|
'Architectures:',
|
||||||
|
'Languages:',
|
||||||
|
'Targets:',
|
||||||
|
'PDiffs:',
|
||||||
|
'By-Hash:',
|
||||||
|
'Allow-Insecure:',
|
||||||
|
'Allow-Weak:',
|
||||||
|
'Allow-Downgrade-To-Insecure:',
|
||||||
|
'Trusted:',
|
||||||
|
'Signed-By:',
|
||||||
|
'Check-Valid-Until:',
|
||||||
|
'Valid-Until-Min:',
|
||||||
|
'Valid-Until-Max:',
|
||||||
|
]
|
||||||
|
|
||||||
|
output_skip_keys = [
|
||||||
|
'X-Repolib-Prefs',
|
||||||
|
'X-Repolib-ID',
|
||||||
|
]
|
||||||
|
|
||||||
|
options_inmap = {
|
||||||
|
'arch': 'Architectures',
|
||||||
|
'lang': 'Languages',
|
||||||
|
'target': 'Targets',
|
||||||
|
'pdiffs': 'PDiffs',
|
||||||
|
'by-hash': 'By-Hash',
|
||||||
|
'allow-insecure': 'Allow-Insecure',
|
||||||
|
'allow-weak': 'Allow-Weak',
|
||||||
|
'allow-downgrade-to-insecure': 'Allow-Downgrade-To-Insecure',
|
||||||
|
'trusted': 'Trusted',
|
||||||
|
'signed-by': 'Signed-By',
|
||||||
|
'check-valid-until': 'Check-Valid-Until',
|
||||||
|
'valid-until-min': 'Valid-Until-Min',
|
||||||
|
'valid-until-max': 'Valid-Until-Max'
|
||||||
|
}
|
||||||
|
|
||||||
|
options_outmap = {
|
||||||
|
'Architectures': 'arch',
|
||||||
|
'Languages': 'lang',
|
||||||
|
'Targets': 'target',
|
||||||
|
'PDiffs': 'pdiffs',
|
||||||
|
'By-Hash': 'by-hash',
|
||||||
|
'Allow-Insecure': 'allow-insecure',
|
||||||
|
'Allow-Weak': 'allow-weak',
|
||||||
|
'Allow-Downgrade-To-Insecure': 'allow-downgrade-to-insecure',
|
||||||
|
'Trusted': 'trusted',
|
||||||
|
'Signed-By': 'signed-by',
|
||||||
|
'Check-Valid-Until': 'check-valid-until',
|
||||||
|
'Valid-Until-Min': 'valid-until-min',
|
||||||
|
'Valid-Until-Max': 'valid-until-max'
|
||||||
|
}
|
||||||
|
|
||||||
|
true_values = [
|
||||||
|
True,
|
||||||
|
'True',
|
||||||
|
'true',
|
||||||
|
'Yes',
|
||||||
|
'yes',
|
||||||
|
'YES',
|
||||||
|
'y',
|
||||||
|
'Y',
|
||||||
|
AptSourceEnabled.TRUE,
|
||||||
|
1
|
||||||
|
]
|
||||||
|
|
||||||
|
keys_map = {
|
||||||
|
'X-Repolib-Name: ': 'Name: ',
|
||||||
|
'X-Repolib-ID: ': 'Ident: ',
|
||||||
|
'X-Repolib-Comments: ': 'Comments: ',
|
||||||
|
'X-Repolib-Default-Mirror: ': 'Default Mirror: ',
|
||||||
|
}
|
||||||
|
|
||||||
|
PRETTY_PRINT = '\n '
|
||||||
|
|
||||||
|
_KEYS_TEMPDIR = tempfile.TemporaryDirectory()
|
||||||
|
TEMP_DIR = Path(_KEYS_TEMPDIR.name)
|
||||||
|
|
||||||
|
options_re = re.compile(r'[^@.+]\[([^[]+.+)\]\ ')
|
||||||
|
uri_re = re.compile(r'\w+:(\/?\/?)[^\s]+')
|
||||||
|
|
||||||
|
CLEAN_CHARS = {
|
||||||
|
33: None,
|
||||||
|
64: 45,
|
||||||
|
35: 45,
|
||||||
|
36: 45,
|
||||||
|
37: 45,
|
||||||
|
94: 45,
|
||||||
|
38: 45,
|
||||||
|
42: 45,
|
||||||
|
41: None,
|
||||||
|
40: None,
|
||||||
|
43: 45,
|
||||||
|
61: 45,
|
||||||
|
91: None,
|
||||||
|
92: None,
|
||||||
|
93: None,
|
||||||
|
123: None,
|
||||||
|
125: None,
|
||||||
|
124: 95,
|
||||||
|
63: None,
|
||||||
|
47: 95,
|
||||||
|
46: 45,
|
||||||
|
60: 95,
|
||||||
|
62: 95,
|
||||||
|
44: 95,
|
||||||
|
96: None,
|
||||||
|
126: None,
|
||||||
|
32: 95,
|
||||||
|
58: None,
|
||||||
|
59: None,
|
||||||
|
}
|
||||||
|
|
||||||
|
sources:dict = {}
|
||||||
|
files:dict = {}
|
||||||
|
keys:dict = {}
|
||||||
|
errors:dict = {}
|
||||||
|
|
||||||
|
|
||||||
|
def scrub_filename(name: str = '') -> str:
|
||||||
|
""" Clean up a string intended for a filename.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
name (str): The prospective name to scrub.
|
||||||
|
|
||||||
|
Returns: str
|
||||||
|
The cleaned-up name.
|
||||||
|
"""
|
||||||
|
return name.translate(CLEAN_CHARS)
|
||||||
|
|
||||||
|
def set_testing(testing:bool=True) -> None:
|
||||||
|
"""Sets Repolib in testing mode where changes will not be saved.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
testing(bool): Whether testing mode should be enabled or disabled
|
||||||
|
(Defaul: True)
|
||||||
|
"""
|
||||||
|
global KEYS_DIR
|
||||||
|
global SOURCES_DIR
|
||||||
|
|
||||||
|
testing_tempdir = tempfile.TemporaryDirectory()
|
||||||
|
|
||||||
|
if not testing:
|
||||||
|
KEYS_DIR = '/usr/share/keyrings'
|
||||||
|
SOURCES_DIR = '/etc/apt/sources.list.d'
|
||||||
|
return
|
||||||
|
|
||||||
|
testing_root = Path(testing_tempdir.name)
|
||||||
|
KEYS_DIR = testing_root / 'usr' / 'share' / 'keyrings'
|
||||||
|
SOURCES_DIR = testing_root / 'etc' / 'apt' / 'sources.list.d'
|
||||||
|
|
||||||
|
|
||||||
|
def _cleanup_temsps() -> None:
|
||||||
|
"""Clean up our tempdir"""
|
||||||
|
_KEYS_TEMPDIR.cleanup()
|
||||||
|
# _TESTING_TEMPDIR.cleanup()
|
||||||
|
|
||||||
|
atexit.register(_cleanup_temsps)
|
||||||
|
|
||||||
|
def dbus_quit():
|
||||||
|
bus = dbus.SystemBus()
|
||||||
|
privileged_object = bus.get_object('org.pop_os.repolib', '/Repo')
|
||||||
|
privileged_object.exit()
|
||||||
|
|
||||||
|
def compare_sources(source1, source2, excl_keys:list) -> bool:
|
||||||
|
"""Compare two sources based on arbitrary criteria.
|
||||||
|
|
||||||
|
This looks at a given list of keys, and if the given keys between the two
|
||||||
|
given sources are identical, returns True.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
source1, source2(Source): The two sources to compare
|
||||||
|
excl_keys([str]): Any keys to exclude from the comparison
|
||||||
|
|
||||||
|
Returns: bool
|
||||||
|
`True` if the sources are identical, otherwise `False`.
|
||||||
|
"""
|
||||||
|
for key in source1:
|
||||||
|
if key in excl_keys:
|
||||||
|
continue
|
||||||
|
if key in source2:
|
||||||
|
if source1[key] != source2[key]:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
for key in source2:
|
||||||
|
if key in excl_keys:
|
||||||
|
continue
|
||||||
|
if key in source1:
|
||||||
|
if source1[key] != source2[key]:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def find_differences_sources(source1, source2, excl_keys:list) -> dict:
|
||||||
|
"""Find key-value pairs which differ between two sources.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
source1, source2(Source): The two sources to compare
|
||||||
|
excl_keys([str]): Any keys to exclude from the comparison
|
||||||
|
|
||||||
|
Returns: dict{'key': ('source1[key]','source2[key]')}
|
||||||
|
The dictionary of different keys, with the key values from each source.
|
||||||
|
"""
|
||||||
|
differing_keys:dict = {}
|
||||||
|
|
||||||
|
for key in source1:
|
||||||
|
if key in excl_keys:
|
||||||
|
continue
|
||||||
|
if key in source2:
|
||||||
|
if source1[key] == source2[key]:
|
||||||
|
continue
|
||||||
|
differing_keys[key] = (source1[key], source2[key])
|
||||||
|
differing_keys[key] = (source1[key], '')
|
||||||
|
for key in source2:
|
||||||
|
if key in excl_keys:
|
||||||
|
continue
|
||||||
|
if key in source1:
|
||||||
|
if source1[key] == source2[key]:
|
||||||
|
continue
|
||||||
|
differing_keys[key] = ('', source2[key])
|
||||||
|
|
||||||
|
return differing_keys
|
||||||
|
|
||||||
|
def combine_sources(source1, source2) -> None:
|
||||||
|
"""Combine the data in two sources into one.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
source1(Source): The source to be merged into
|
||||||
|
source2(Source): The source to merge from
|
||||||
|
"""
|
||||||
|
for key in source1:
|
||||||
|
if key in ('X-Repolib-Name', 'X-Repolib-ID', 'Enabled', 'Types'):
|
||||||
|
continue
|
||||||
|
if key in source2:
|
||||||
|
source1[key] += f' {source2[key]}'
|
||||||
|
for key in source2:
|
||||||
|
if key in ('X-Repolib-Name', 'X-Repolib-ID', 'Enabled', 'Types'):
|
||||||
|
continue
|
||||||
|
if key in source1:
|
||||||
|
source1[key] += f' {source2[key]}'
|
||||||
|
|
||||||
|
# Need to deduplicate the list
|
||||||
|
for key in source1:
|
||||||
|
vals = source1[key].strip().split()
|
||||||
|
newvals = []
|
||||||
|
for val in vals:
|
||||||
|
if val not in newvals:
|
||||||
|
newvals.append(val)
|
||||||
|
source1[key] = ' '.join(newvals)
|
||||||
|
for key in source2:
|
||||||
|
vals = source2[key].strip().split()
|
||||||
|
newvals = []
|
||||||
|
for val in vals:
|
||||||
|
if val not in newvals:
|
||||||
|
newvals.append(val)
|
||||||
|
source2[key] = ' '.join(newvals)
|
||||||
|
|
||||||
|
|
||||||
|
def prettyprint_enable(enabled: bool = True) -> None:
|
||||||
|
"""Easy helper to enable/disable pretty-printing for object reprs.
|
||||||
|
|
||||||
|
Can also be used as an easy way to reset to defaults.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
enabled(bool): Whether or not Pretty Printing should be enabled
|
||||||
|
"""
|
||||||
|
global PRETTY_PRINT
|
||||||
|
if enabled:
|
||||||
|
PRETTY_PRINT = '\n '
|
||||||
|
else:
|
||||||
|
PRETTY_PRINT = ''
|
||||||
|
|
||||||
|
def url_validator(url):
|
||||||
|
""" Validate a url and tell if it's good or not.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
url (str): The URL to validate.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
`True` if `url` is not malformed, otherwise `False`.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# pylint: disable=no-else-return,bare-except
|
||||||
|
# A) We want to return false if the URL doesn't contain those parts
|
||||||
|
# B) We need this to not throw any exceptions, regardless what they are
|
||||||
|
result = urlparse(url)
|
||||||
|
if not result.scheme:
|
||||||
|
return False
|
||||||
|
if result.scheme == 'x-repolib-name':
|
||||||
|
return False
|
||||||
|
if result.netloc:
|
||||||
|
# We need at least a scheme and a netlocation/hostname or...
|
||||||
|
return all([result.scheme, result.netloc])
|
||||||
|
elif result.path:
|
||||||
|
# ...a scheme and a path (this allows file:/// URIs which are valid)
|
||||||
|
return all([result.scheme, result.path])
|
||||||
|
return False
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def validate_debline(valid):
|
||||||
|
""" Basic checks to see if a given debline is valid or not.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
valid (str): The line to validate.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if the line is valid, False otherwise.
|
||||||
|
"""
|
||||||
|
comment:bool = False
|
||||||
|
if valid.startswith('#'):
|
||||||
|
comment = True
|
||||||
|
valid = valid.replace('#', '')
|
||||||
|
valid = valid.strip()
|
||||||
|
|
||||||
|
if valid.startswith("deb"):
|
||||||
|
words = valid.split()
|
||||||
|
for word in words:
|
||||||
|
if url_validator(word):
|
||||||
|
return True
|
||||||
|
|
||||||
|
elif valid.startswith("ppa:"):
|
||||||
|
if "/" in valid:
|
||||||
|
return True
|
||||||
|
|
||||||
|
else:
|
||||||
|
if valid.endswith('.flatpakrepo'):
|
||||||
|
return False
|
||||||
|
if len(valid.split()) == 1 and not comment:
|
||||||
|
return url_validator(valid)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def strip_hashes(line:str) -> str:
|
||||||
|
""" Strips the leading #'s from the given line.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
line (str): The line to strip.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(str): The input line without any leading/trailing hashes or
|
||||||
|
leading/trailing whitespace.
|
||||||
|
"""
|
||||||
|
while True:
|
||||||
|
line = line.strip('#')
|
||||||
|
line = line.strip()
|
||||||
|
if not line.startswith('#'):
|
||||||
|
break
|
||||||
|
|
||||||
|
return line
|
194
repolib/usr/lib/repolib/add-apt-repository
Executable file
194
repolib/usr/lib/repolib/add-apt-repository
Executable file
@ -0,0 +1,194 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
"""
|
||||||
|
Copyright (c) 2019-2020, Ian Santopietro
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
This file is part of RepoLib.
|
||||||
|
|
||||||
|
RepoLib is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
RepoLib is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public License
|
||||||
|
along with RepoLib. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
"""
|
||||||
|
#pylint: disable=invalid-name
|
||||||
|
# Pylint will complain about our module name not being snake_case, however this
|
||||||
|
# is a command rather than a python module, and thus this is correct anyway.
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
import repolib
|
||||||
|
|
||||||
|
system_codename = repolib.util.DISTRO_CODENAME
|
||||||
|
|
||||||
|
system_components = [
|
||||||
|
'main',
|
||||||
|
'universe',
|
||||||
|
'multiverse',
|
||||||
|
'restricted'
|
||||||
|
]
|
||||||
|
|
||||||
|
system_suites = [
|
||||||
|
system_codename,
|
||||||
|
f'{system_codename}-updates',
|
||||||
|
f'{system_codename}-security',
|
||||||
|
f'{system_codename}-backports',
|
||||||
|
f'{system_codename}-proposed',
|
||||||
|
'updates',
|
||||||
|
'security',
|
||||||
|
'backports',
|
||||||
|
'proposed',
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_args():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
prog='add-apt-repository',
|
||||||
|
description=(
|
||||||
|
'add-apt-repository is a script for adding apt sources.list entries.'
|
||||||
|
'\nThis command has been deprecated in favor of `apt-manage`. See '
|
||||||
|
'`apt-manage --help` for more information.'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'sourceline',
|
||||||
|
metavar='<sourceline>'
|
||||||
|
)
|
||||||
|
|
||||||
|
group = parser.add_mutually_exclusive_group()
|
||||||
|
|
||||||
|
group.add_argument(
|
||||||
|
'-m',
|
||||||
|
'--massive-debug',
|
||||||
|
dest='debug',
|
||||||
|
action='store_true',
|
||||||
|
help='Print a lot of debug information to the command line'
|
||||||
|
)
|
||||||
|
|
||||||
|
group.add_argument(
|
||||||
|
'-r',
|
||||||
|
'--remove',
|
||||||
|
action='store_true',
|
||||||
|
help='remove repository from sources.list.d directory'
|
||||||
|
)
|
||||||
|
|
||||||
|
group.add_argument(
|
||||||
|
'-s',
|
||||||
|
'--enable-source',
|
||||||
|
dest='source',
|
||||||
|
action='store_true',
|
||||||
|
help='Allow downloading of source packages from the repository'
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'-y',
|
||||||
|
'--yes',
|
||||||
|
action='store_true',
|
||||||
|
help='Assum yes to all queries'
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'-n',
|
||||||
|
'--no-update',
|
||||||
|
dest='noupdate',
|
||||||
|
action='store_true',
|
||||||
|
help='Do not update package cache after adding'
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'-u',
|
||||||
|
'--update',
|
||||||
|
action='store_true',
|
||||||
|
help='Update package cache after adding (legacy option)'
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'-k',
|
||||||
|
'--keyserver',
|
||||||
|
metavar='KEYSERVER',
|
||||||
|
help='Legacy option, unused.'
|
||||||
|
)
|
||||||
|
|
||||||
|
return parser
|
||||||
|
|
||||||
|
parser = get_args()
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
command = ['apt-manage']
|
||||||
|
|
||||||
|
if args.debug:
|
||||||
|
command.append('-bb')
|
||||||
|
|
||||||
|
sourceline = args.sourceline
|
||||||
|
run = True
|
||||||
|
remove = False
|
||||||
|
|
||||||
|
if sourceline in system_components:
|
||||||
|
command.append('modify')
|
||||||
|
command.append('system')
|
||||||
|
if not args.remove:
|
||||||
|
command.append('--add-component')
|
||||||
|
else:
|
||||||
|
command.append('--remove-component')
|
||||||
|
|
||||||
|
elif sourceline in system_suites:
|
||||||
|
command.append('modify')
|
||||||
|
command.append('system')
|
||||||
|
if not args.remove:
|
||||||
|
command.append('--add-suite')
|
||||||
|
else:
|
||||||
|
command.append('--remove-suite')
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
if args.source:
|
||||||
|
command.append('source')
|
||||||
|
|
||||||
|
elif args.remove:
|
||||||
|
remove = True
|
||||||
|
command.append('remove')
|
||||||
|
|
||||||
|
else:
|
||||||
|
command.append('add')
|
||||||
|
if not args.yes:
|
||||||
|
command.append('--expand')
|
||||||
|
|
||||||
|
if not remove:
|
||||||
|
command.append(sourceline)
|
||||||
|
else:
|
||||||
|
sources = repolib.get_all_sources()
|
||||||
|
comp_source = repolib.DebLine(sourceline)
|
||||||
|
for source in sources:
|
||||||
|
if comp_source.uris[0] in source.uris:
|
||||||
|
name = str(source.filename.name)
|
||||||
|
name = name.replace(".list", "")
|
||||||
|
name = name.replace(".sources", "")
|
||||||
|
command.append(name)
|
||||||
|
|
||||||
|
run = True
|
||||||
|
|
||||||
|
if os.geteuid() != 0:
|
||||||
|
print('Error: must run as root')
|
||||||
|
run = False
|
||||||
|
|
||||||
|
if run:
|
||||||
|
subprocess.run(command)
|
||||||
|
|
||||||
|
if not args.noupdate:
|
||||||
|
subprocess.run(['apt', 'update'])
|
||||||
|
|
||||||
|
print('NOTE: add-apt-repository is deprecated in Pop!_OS. Use this instead:')
|
||||||
|
print_command = command.copy()
|
||||||
|
if '--expand' in print_command:
|
||||||
|
print_command.remove('--expand')
|
||||||
|
print(' '.join(print_command))
|
316
repolib/usr/lib/repolib/service.py
Executable file
316
repolib/usr/lib/repolib/service.py
Executable file
@ -0,0 +1,316 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
'''
|
||||||
|
Copyright 2020 Ian Santopietro (ian@system76.com)
|
||||||
|
|
||||||
|
This file is part of Repolib.
|
||||||
|
|
||||||
|
Repolib is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Repolib is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with Repolib. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
'''
|
||||||
|
#pylint: skip-file
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
import gi
|
||||||
|
from gi.repository import GObject, GLib
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
import dbus
|
||||||
|
import dbus.service
|
||||||
|
import dbus.mainloop.glib
|
||||||
|
import time
|
||||||
|
|
||||||
|
import repolib
|
||||||
|
|
||||||
|
class RepolibException(dbus.DBusException):
|
||||||
|
_dbus_error_name = 'org.pop_os.repolib.RepolibException'
|
||||||
|
|
||||||
|
class PermissionDeniedByPolicy(dbus.DBusException):
|
||||||
|
_dbus_error_name = 'org.pop_os.repolib.PermissionDeniedByPolicy'
|
||||||
|
|
||||||
|
class AptException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Repo(dbus.service.Object):
|
||||||
|
def __init__(self, conn=None, object_path=None, bus_name=None):
|
||||||
|
dbus.service.Object.__init__(self, conn, object_path, bus_name)
|
||||||
|
|
||||||
|
# These are used by PolKit to check privileges
|
||||||
|
self.dbus_info = None
|
||||||
|
self.polkit = None
|
||||||
|
self.enforce_polkit = True
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.system_repo = repolib.SystemSource()
|
||||||
|
except:
|
||||||
|
self.system_repo = None
|
||||||
|
|
||||||
|
self.source = None
|
||||||
|
self.sources_dir = Path('/etc/apt/sources.list.d')
|
||||||
|
self.keys_dir = Path('/etc/apt/trusted.gpg.d')
|
||||||
|
|
||||||
|
@dbus.service.method(
|
||||||
|
"org.pop_os.repolib.Interface",
|
||||||
|
in_signature='as', out_signature='',
|
||||||
|
sender_keyword='sender', connection_keyword='conn'
|
||||||
|
)
|
||||||
|
def add_apt_signing_key(self, cmd, sender=None, conn=None):
|
||||||
|
self._check_polkit_privilege(
|
||||||
|
sender, conn, 'org.pop_os.repolib.modifysources'
|
||||||
|
)
|
||||||
|
print(cmd)
|
||||||
|
key_path = str(cmd.pop(-1))
|
||||||
|
with open(key_path, mode='wb') as keyfile:
|
||||||
|
try:
|
||||||
|
subprocess.run(cmd, check=True, stdout=keyfile)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
raise e
|
||||||
|
|
||||||
|
@dbus.service.method(
|
||||||
|
"org.pop_os.repolib.Interface",
|
||||||
|
in_signature='ss', out_signature='',
|
||||||
|
sender_keyword='sender', connection_keyword='conn'
|
||||||
|
)
|
||||||
|
def install_signing_key(self, src, dest, sender=None, conn=None):
|
||||||
|
self._check_polkit_privilege(
|
||||||
|
sender, conn, 'org.pop_os.repolib.modifysources'
|
||||||
|
)
|
||||||
|
shutil.copy2(src, dest)
|
||||||
|
|
||||||
|
@dbus.service.method(
|
||||||
|
"org.pop_os.repolib.Interface",
|
||||||
|
in_signature='s', out_signature='',
|
||||||
|
sender_keyword='sender', connection_keyword='conn'
|
||||||
|
)
|
||||||
|
def delete_signing_key(self, src, sender=None, conn=None):
|
||||||
|
self._check_polkit_privilege(
|
||||||
|
sender, conn, 'org.pop_os.repolib.modifysources'
|
||||||
|
)
|
||||||
|
key_path = Path(src)
|
||||||
|
key_path.unlink()
|
||||||
|
|
||||||
|
@dbus.service.method(
|
||||||
|
"org.pop_os.repolib.Interface",
|
||||||
|
in_signature='s', out_signature='',
|
||||||
|
sender_keyword='sender', connection_keyword='conn'
|
||||||
|
)
|
||||||
|
def delete_prefs_file(self, src, sender=None, conn=None):
|
||||||
|
self._check_polkit_privilege(
|
||||||
|
sender, conn, 'org.pop_os.repolib.modifysources'
|
||||||
|
)
|
||||||
|
prefs_path = Path(src)
|
||||||
|
prefs_path.unlink()
|
||||||
|
|
||||||
|
@dbus.service.method(
|
||||||
|
"org.pop_os.repolib.Interface",
|
||||||
|
in_signature='ss', out_signature='',
|
||||||
|
sender_keyword='sender', connection_keyword='conn'
|
||||||
|
)
|
||||||
|
def output_prefs_to_disk(self, path, contents, sender=None, conn=None):
|
||||||
|
self._check_polkit_privilege(
|
||||||
|
sender, conn, 'org.pop_os.repolib.modifysources'
|
||||||
|
)
|
||||||
|
full_path = Path(path)
|
||||||
|
with open(full_path, mode='w') as output_file:
|
||||||
|
output_file.write(contents)
|
||||||
|
|
||||||
|
@dbus.service.method(
|
||||||
|
"org.pop_os.repolib.Interface",
|
||||||
|
in_signature='ss', out_signature='',
|
||||||
|
sender_keyword='sender', connection_keyword='conn'
|
||||||
|
)
|
||||||
|
def output_file_to_disk(self, filename, source, sender=None, conn=None):
|
||||||
|
self._check_polkit_privilege(
|
||||||
|
sender, conn, 'org.pop_os.repolib.modifysources'
|
||||||
|
)
|
||||||
|
full_path = self.sources_dir / filename
|
||||||
|
with open(full_path, mode='w') as output_file:
|
||||||
|
output_file.write(source)
|
||||||
|
|
||||||
|
@dbus.service.method(
|
||||||
|
"org.pop_os.repolib.Interface",
|
||||||
|
in_signature='ss', out_signature='',
|
||||||
|
sender_keyword='sender', connection_keyword='conn'
|
||||||
|
)
|
||||||
|
def backup_alt_file(self, alt_file, save_file, sender=None, conn=None):
|
||||||
|
self._check_polkit_privilege(
|
||||||
|
sender, conn, 'org.pop_os.repolib.modifysources'
|
||||||
|
)
|
||||||
|
alt_path = self.sources_dir / alt_file
|
||||||
|
save_path = self.sources_dir / save_file
|
||||||
|
if alt_path.exists():
|
||||||
|
alt_path.rename(save_path)
|
||||||
|
|
||||||
|
@dbus.service.method(
|
||||||
|
"org.pop_os.repolib.Interface",
|
||||||
|
in_signature='s', out_signature='',
|
||||||
|
sender_keyword='sender', connection_keyword='conn'
|
||||||
|
)
|
||||||
|
def delete_source_file(self, filename, sender=None, conn=None):
|
||||||
|
self._check_polkit_privilege(
|
||||||
|
sender, conn, 'org.pop_os.repolib.modifysources'
|
||||||
|
)
|
||||||
|
source_file = self.sources_dir / filename
|
||||||
|
source_file.unlink()
|
||||||
|
|
||||||
|
@dbus.service.method(
|
||||||
|
"org.pop_os.repolib.Interface",
|
||||||
|
in_signature='', out_signature='',
|
||||||
|
sender_keyword='sender', connection_keyword='conn'
|
||||||
|
)
|
||||||
|
def exit(self, sender=None, conn=None):
|
||||||
|
mainloop.quit()
|
||||||
|
|
||||||
|
@dbus.service.method(
|
||||||
|
"org.pop_os.repolib.Interface",
|
||||||
|
in_signature='b', out_signature='b',
|
||||||
|
sender_keyword='sender', connection_keyword='conn'
|
||||||
|
)
|
||||||
|
def set_system_source_code_enabled(self, enabled, sender=None, conn=None):
|
||||||
|
""" Enable or disable source code in the system source.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
enabled (bool): The new state to set, True = Enabled.
|
||||||
|
"""
|
||||||
|
self._check_polkit_privilege(
|
||||||
|
sender, conn, 'org.pop_os.repolib.modifysources'
|
||||||
|
)
|
||||||
|
if self.system_repo:
|
||||||
|
self.system_repo.load_from_file()
|
||||||
|
new_types = [repolib.util.AptSourceType.BINARY]
|
||||||
|
if enabled:
|
||||||
|
new_types.append(repolib.util.AptSourceType.SOURCE)
|
||||||
|
self.system_repo.types = new_types
|
||||||
|
self.system_repo.save_to_disk()
|
||||||
|
return enabled
|
||||||
|
return False
|
||||||
|
|
||||||
|
@dbus.service.method(
|
||||||
|
"org.pop_os.repolib.Interface",
|
||||||
|
in_signature='sb', out_signature='b',
|
||||||
|
sender_keyword='sender', connection_keyword='conn'
|
||||||
|
)
|
||||||
|
def set_system_comp_enabled(self, comp, enable, sender=None, conn=None):
|
||||||
|
""" Enable or disable a component in the system source.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
comp (str): the component to set
|
||||||
|
enable (bool): The new state to set, True = Enabled.
|
||||||
|
"""
|
||||||
|
self._check_polkit_privilege(
|
||||||
|
sender, conn, 'org.pop_os.repolib.modifysources'
|
||||||
|
)
|
||||||
|
if self.system_repo:
|
||||||
|
self.system_repo.load_from_file()
|
||||||
|
self.system_repo.set_component_enabled(component=comp, enabled=enable)
|
||||||
|
self.system_repo.save_to_disk()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
@dbus.service.method(
|
||||||
|
"org.pop_os.repolib.Interface",
|
||||||
|
in_signature='sb', out_signature='b',
|
||||||
|
sender_keyword='sender', connection_keyword='conn'
|
||||||
|
)
|
||||||
|
def set_system_suite_enabled(self, suite, enable, sender=None, conn=None):
|
||||||
|
""" Enable or disable a suite in the system source.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
suite (str): the suite to set
|
||||||
|
enable (bool): The new state to set, True = Enabled.
|
||||||
|
"""
|
||||||
|
self._check_polkit_privilege(
|
||||||
|
sender, conn, 'org.pop_os.repolib.modifysources'
|
||||||
|
)
|
||||||
|
if self.system_repo:
|
||||||
|
self.system_repo.load_from_file()
|
||||||
|
self.system_repo.set_suite_enabled(suite=suite, enabled=enable)
|
||||||
|
self.system_repo.save_to_disk()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _log_in_file(klass, filename, string):
|
||||||
|
date = time.asctime(time.localtime())
|
||||||
|
ff = open(filename, "a")
|
||||||
|
ff.write("%s : %s\n" %(date,str(string)))
|
||||||
|
ff.close()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _strip_source_line(self, source):
|
||||||
|
source = source.replace("#", "# ")
|
||||||
|
source = source.replace("[", "")
|
||||||
|
source = source.replace("]", "")
|
||||||
|
source = source.replace("'", "")
|
||||||
|
source = source.replace(" ", " ")
|
||||||
|
return source
|
||||||
|
|
||||||
|
def _check_polkit_privilege(self, sender, conn, privilege):
|
||||||
|
# from jockey
|
||||||
|
'''Verify that sender has a given PolicyKit privilege.
|
||||||
|
sender is the sender's (private) D-BUS name, such as ":1:42"
|
||||||
|
(sender_keyword in @dbus.service.methods). conn is
|
||||||
|
the dbus.Connection object (connection_keyword in
|
||||||
|
@dbus.service.methods). privilege is the PolicyKit privilege string.
|
||||||
|
This method returns if the caller is privileged, and otherwise throws a
|
||||||
|
PermissionDeniedByPolicy exception.
|
||||||
|
'''
|
||||||
|
if sender is None and conn is None:
|
||||||
|
# called locally, not through D-BUS
|
||||||
|
return
|
||||||
|
if not self.enforce_polkit:
|
||||||
|
# that happens for testing purposes when running on the session
|
||||||
|
# bus, and it does not make sense to restrict operations here
|
||||||
|
return
|
||||||
|
|
||||||
|
# get peer PID
|
||||||
|
if self.dbus_info is None:
|
||||||
|
self.dbus_info = dbus.Interface(conn.get_object('org.freedesktop.DBus',
|
||||||
|
'/org/freedesktop/DBus/Bus', False), 'org.freedesktop.DBus')
|
||||||
|
pid = self.dbus_info.GetConnectionUnixProcessID(sender)
|
||||||
|
|
||||||
|
# query PolicyKit
|
||||||
|
if self.polkit is None:
|
||||||
|
self.polkit = dbus.Interface(dbus.SystemBus().get_object(
|
||||||
|
'org.freedesktop.PolicyKit1',
|
||||||
|
'/org/freedesktop/PolicyKit1/Authority', False),
|
||||||
|
'org.freedesktop.PolicyKit1.Authority')
|
||||||
|
try:
|
||||||
|
# we don't need is_challenge return here, since we call with AllowUserInteraction
|
||||||
|
(is_auth, _, details) = self.polkit.CheckAuthorization(
|
||||||
|
('unix-process', {'pid': dbus.UInt32(pid, variant_level=1),
|
||||||
|
'start-time': dbus.UInt64(0, variant_level=1)}),
|
||||||
|
privilege, {'': ''}, dbus.UInt32(1), '', timeout=600)
|
||||||
|
except dbus.DBusException as e:
|
||||||
|
if e._dbus_error_name == 'org.freedesktop.DBus.Error.ServiceUnknown':
|
||||||
|
# polkitd timed out, connect again
|
||||||
|
self.polkit = None
|
||||||
|
return self._check_polkit_privilege(sender, conn, privilege)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
if not is_auth:
|
||||||
|
Repo._log_in_file('/tmp/repolib.log','_check_polkit_privilege: sender %s on connection %s pid %i is not authorized for %s: %s' %
|
||||||
|
(sender, conn, pid, privilege, str(details)))
|
||||||
|
raise PermissionDeniedByPolicy(privilege)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
|
||||||
|
|
||||||
|
bus = dbus.SystemBus()
|
||||||
|
name = dbus.service.BusName("org.pop_os.repolib", bus)
|
||||||
|
object = Repo(bus, '/Repo')
|
||||||
|
|
||||||
|
mainloop = GLib.MainLoop()
|
||||||
|
mainloop.run()
|
42
repolib/usr/share/bash-completion/completions/apt-manage
Executable file
42
repolib/usr/share/bash-completion/completions/apt-manage
Executable file
@ -0,0 +1,42 @@
|
|||||||
|
# Debian apt-manage completion
|
||||||
|
|
||||||
|
_apt_manage()
|
||||||
|
{
|
||||||
|
local cur prev words cword package
|
||||||
|
_init_completion -n ':=' || return
|
||||||
|
|
||||||
|
local special i
|
||||||
|
i=0
|
||||||
|
for (( i=0; i < ${#words[@]}-1; i++ )); do
|
||||||
|
if [[ ${words[i]} == @(add|list|modify|remove|key) ]]; then
|
||||||
|
special=${words[i]}
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -n $special ]]; then
|
||||||
|
case $special in
|
||||||
|
list|modify|remove|key)
|
||||||
|
COMPREPLY=( $( compgen -W '$(apt-manage list -n)' -- "$cur" ) )
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
if [[ "$cur" == -* ]]; then
|
||||||
|
return
|
||||||
|
# COMPREPLY=( $(compgen -W '
|
||||||
|
# --help --disable --source-code --expand
|
||||||
|
# --verbose --legacy --no-names
|
||||||
|
# --enable --disable --name --add-suite --remove-suite
|
||||||
|
# --add-component --remove-component --add-uri --remove-uri
|
||||||
|
# ' -- "$cur") )
|
||||||
|
else
|
||||||
|
COMPREPLY=( $(compgen -W 'add list modify remove key' \
|
||||||
|
-- "$cur") )
|
||||||
|
fi
|
||||||
|
|
||||||
|
} &&
|
||||||
|
complete -F _apt_manage apt-manage
|
@ -0,0 +1,4 @@
|
|||||||
|
[D-BUS Service]
|
||||||
|
Name=org.pop_os.repolib
|
||||||
|
Exec=/usr/bin/python3 /usr/lib/repolib/service.py
|
||||||
|
User=root
|
20
repolib/usr/share/polkit-1/actions/org.pop_os.repolib.policy
Normal file
20
repolib/usr/share/polkit-1/actions/org.pop_os.repolib.policy
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE policyconfig PUBLIC
|
||||||
|
"-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
|
||||||
|
"http://www.freedesktop.org/standards/PolicyKit/1.0/policyconfig.dtd">
|
||||||
|
<policyconfig>
|
||||||
|
|
||||||
|
<vendor>Repoman</vendor>
|
||||||
|
<vendor_url>https://github.com/pop-os/repolib</vendor_url>
|
||||||
|
<icon_name>x-system-software-sources</icon_name>
|
||||||
|
|
||||||
|
<action id="org.pop_os.repolib.modifysources">
|
||||||
|
<description>Modifies a Debian Repository in the system software sources.</description>
|
||||||
|
<message>Authentication is required to change software sources.</message>
|
||||||
|
<defaults>
|
||||||
|
<allow_any>auth_admin_keep</allow_any>
|
||||||
|
<allow_inactive>auth_admin_keep</allow_inactive>
|
||||||
|
<allow_active>auth_admin_keep</allow_active>
|
||||||
|
</defaults>
|
||||||
|
</action>
|
||||||
|
</policyconfig>
|
71
repolib/usr/share/zsh/vendor-completions/_apt-manage
Normal file
71
repolib/usr/share/zsh/vendor-completions/_apt-manage
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
#compdef apt-manage
|
||||||
|
typeset -A opt_args
|
||||||
|
|
||||||
|
typeset -A opt_args
|
||||||
|
|
||||||
|
_arguments -C \
|
||||||
|
'1:cmd:->cmds' \
|
||||||
|
'2:sources:->source_lists' \
|
||||||
|
'*:: :->args' \
|
||||||
|
&& ret=0
|
||||||
|
|
||||||
|
case "$state" in
|
||||||
|
(cmds)
|
||||||
|
local commands; commands=(
|
||||||
|
'add'
|
||||||
|
'list'
|
||||||
|
'modify'
|
||||||
|
'remove'
|
||||||
|
'key'
|
||||||
|
)
|
||||||
|
_describe -t commands 'command' commands && ret=0
|
||||||
|
;;
|
||||||
|
(source_lists)
|
||||||
|
local sources
|
||||||
|
sources=( $(apt-manage list -n))
|
||||||
|
_describe -t sources 'source' sources && ret=0
|
||||||
|
;;
|
||||||
|
(args)
|
||||||
|
local arguments
|
||||||
|
arguments=(
|
||||||
|
# Main command
|
||||||
|
'--help'
|
||||||
|
# Add subcommand
|
||||||
|
'--disable'
|
||||||
|
'--source-code'
|
||||||
|
'--terse'
|
||||||
|
'--name'
|
||||||
|
'--identifier'
|
||||||
|
'--format'
|
||||||
|
'--skip-keys'
|
||||||
|
# Key Ssbcommand
|
||||||
|
'--path'
|
||||||
|
'--url'
|
||||||
|
'--ascii'
|
||||||
|
'--fingerprint'
|
||||||
|
'--keyserver'
|
||||||
|
'--info'
|
||||||
|
'--remove'
|
||||||
|
# List subcommand
|
||||||
|
'--legacy'
|
||||||
|
'--verbose'
|
||||||
|
'--all'
|
||||||
|
'--file-names'
|
||||||
|
# Modify subcommand
|
||||||
|
'--enable'
|
||||||
|
'--source-enable'
|
||||||
|
'--source-disable'
|
||||||
|
'--add-uri'
|
||||||
|
'--add-suite'
|
||||||
|
'--add-component'
|
||||||
|
'--remove-uri'
|
||||||
|
'--remove-suite'
|
||||||
|
'--remove-component'
|
||||||
|
# Remove subcommand
|
||||||
|
'--assume-yes'
|
||||||
|
)
|
||||||
|
_describe -t arguments 'argument' arguments && ret=0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
return 1
|
Loading…
Reference in New Issue
Block a user