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
|
||||
|
||||
### WTF ###
|
||||
# See debhelper(7) (uncomment to enable)
|
||||
# output every command that modifies files on the build system.
|
||||
#export DH_VERBOSE = 1
|
||||
|
||||
export PYBUILD_NAME=repolib
|
||||
export PYBUILD_OPTION=--test-pytest
|
||||
|
||||
%:
|
||||
dh $@ --with python3 --buildsystem=pybuild
|
||||
|
||||
#
|
||||
#export PYBUILD_NAME=repolib
|
||||
#export PYBUILD_OPTION=--test-pytest
|
||||
#
|
||||
#%:
|
||||
# dh $@ --with python3 --buildsystem=pybuild
|
||||
#
|
||||
## Uncomment to disable testing during package builds
|
||||
## NOTE for QA or Engineering Review: This should not be uncommented in a
|
||||
## PR. If it is, DO NOT APPROVE THE PR!!!
|
||||
@ -21,4 +23,7 @@ export PYBUILD_OPTION=--test-pytest
|
||||
# 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 -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:kubuntu-ppa/backports
|
||||
# Clone Upstream
|
||||
git clone https://github.com/pop-os/repolib
|
||||
rm -rvf ./repolib/debian
|
||||
cp -rvf ./debian ./repolib
|
||||
### WTF ###
|
||||
#git clone https://github.com/pop-os/repolib
|
||||
#rm -rvf ./repolib/debian
|
||||
### WTF ###
|
||||
cp -rvf ./python3-repolib.install ./debian/
|
||||
cp -rvf ./debian ./repolib/
|
||||
cd ./repolib
|
||||
|
||||
# Get build deps
|
||||
apt-get build-dep ./ -y
|
||||
|
||||
# Build package
|
||||
dpkg-buildpackage
|
||||
dpkg-buildpackage --no-sign
|
||||
|
||||
# Move the debs to output
|
||||
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