diff --git a/arch.py b/arch.py new file mode 100644 index 0000000..a1383d8 --- /dev/null +++ b/arch.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + + +import os +import tarfile + + + + +class ArchDatabase: + """Representation of an Arch Linux package database.""" + FILE_EXTENSION = ".db.tar.gz" + + + def __init__(self, filename): + """Construct a new database object and read all containing packages.""" + self.filename = filename + self.packages = [] + self._parse() + + + def get_fielname(self): + """Get filename of database.""" + return self.filename + + + def get_name(self): + """Get simple name of database.""" + return os.path.basename(self.filename)[0:-len(ArchDatabase.FILE_EXTENSION)] + + + def get_packages(self): + """Get list of packages contained in the database.""" + return self.packages + + + def _parse(self): + """Read and parse database content.""" + with tarfile.open(name=self.filename) as tf: + for f in tf: + if f.isfile() and os.path.basename(f.name) == "desc": + df = tf.extractfile(f) + s = df.read().decode('utf-8') + + d = dict(item.split("\n")[:2] for item in s.split("\n\n")[:-1]) + archPackage = ArchPackage(d["%NAME%"]) + archPackage.set_filename(d["%FILENAME%"]) + archPackage.set_version(d["%VERSION%"]) + archPackage.set_desc(d["%DESC%"]) + archPackage.set_csize(d["%CSIZE%"]) + archPackage.set_isize(d["%ISIZE%"]) + archPackage.set_url(d["%URL%"]) + archPackage.set_license(d["%LICENSE%"]) + archPackage.set_arch(d["%ARCH%"]) + archPackage.set_builddate(d["%BUILDDATE%"]) + archPackage.set_packager(d["%PACKAGER%"]) + self.packages.append(archPackage) + + + def find_databases(directory): + """Find all database files in a directory, including subdirectories.""" + databases = [] + for root, dirs, files in os.walk(directory): + for file in files: + if file.endswith(ArchDatabase.FILE_EXTENSION): + archDatabase = ArchDatabase(os.path.join(root, file)) + databases.append(archDatabase) + return databases + + + + +class ArchPackage: + """Representation of an Arch Linux package.""" + + + def __init__(self, name): + """Construct a new package.""" + self.name = name + self.filename = None + self.version = None + self.desc = None + self.csize = None + self.isize = None + self.url = None + self.license = None + self.arch = None + self.builddate = None + self.packager = None + + + def get_name(self): + """Get the package name.""" + return self.name + + + def set_filename(self, filename): + """Set package filename.""" + self.filename = filename + + + def get_filename(self): + """Get package filename.""" + return self.filename + + + def set_version(self, version): + """Set version.""" + self.version = version + + + def get_version(self): + """Get version.""" + return self.version + + + def set_desc(self, desc): + """Set description.""" + self.desc = desc + + + def get_desc(self): + """Get description.""" + return self.desc + + + def set_csize(self, csize): + """Set csize.""" + self.csize = csize + + + def get_csize(self): + """Get csize.""" + return self.csize + + + def set_isize(self, isize): + """Set isize.""" + self.isize = isize + + + def get_isize(self): + """Get isize.""" + return self.isize + + + def set_url(self, url): + """Set upstream URL.""" + self.url = url + + + def get_url(self): + """Get upstream URL.""" + return self.url + + + def set_license(self, license): + """Set license.""" + self.license = license + + + def get_license(self): + """Get license.""" + return self.license + + + def set_arch(self, arch): + """Set architecture.""" + self.arch = arch + + + def get_arch(self): + """Get architecture.""" + return self.arch + + + def set_builddate(self, builddate): + """Set build date.""" + self.builddate = builddate + + + def get_builddate(self): + """Get build date.""" + return self.builddate + + + def set_packager(self, packager): + """Set name of packager.""" + self.packager = packager + + + def get_packager(self): + """Get name of packager.""" + return self.packager diff --git a/aur-check.py b/aur-check.py new file mode 100755 index 0000000..224360c --- /dev/null +++ b/aur-check.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + + +import argparse +import os +import subprocess + +from arch import ArchDatabase, ArchPackage +from aur import AURPackage + + + + +class Styling: + """Output styling constants.""" + ENDC = '\033[0m' + BOLD = '\033[1m' + UPTODATE = '\033[32m' + NEEDS_UPDATE = '\033[31m' + FLAGGED = '\033[33m' + NEEDS_DOWNGRADE = '\033[34m' + URL = '\033[37m' + + + + +class AURChecker: + """ + Read local Arch Linux package databases/repositories and compare the + version of each containing package against the AUR package with the same + name to determine and print a status for each package. + """ + STATUS_UPTODATE = "uptodate" + STATUS_NEEDS_UPDATE = "needs update" + STATUS_NEEDS_DOWNGRADE = "needs downgrade" + + + def check(directory): + """Check all databases/repositories in a directory.""" + databases = ArchDatabase.find_databases(directory) + for database in databases: + AURChecker.check_database(database) + + + def check_database(database): + """Check a database/repository.""" + print(Styling.BOLD + "# repository {}".format(database.get_name()) + Styling.ENDC) + for package in database.get_packages(): + aur_package = AURPackage(package.get_name()) + status = AURChecker.compare(package, aur_package) + + status_messages = {} + status_messages[AURChecker.STATUS_UPTODATE] = Styling.UPTODATE + "up-do-date" + Styling.ENDC + status_messages[AURChecker.STATUS_NEEDS_UPDATE] = Styling.NEEDS_UPDATE + "needs update to {}\n".format(aur_package.get_version()) + Styling.URL + " {}{}".format(AURPackage.AUR_URL, aur_package.get_url_path()) + Styling.ENDC + status_messages[AURChecker.STATUS_NEEDS_DOWNGRADE] = Styling.NEEDS_DOWNGRADE + "local is newer" + Styling.ENDC + + message = " – {} {}: {}".format(package.get_name(), package.get_version(), status_messages[status]) + if aur_package.get_out_of_date(): + message = Styling.FLAGGED + "{} (flagged)".format(message) + Styling.ENDC + print(message) + + + def compare(package, aur_package): + """Compare package two versions and return status.""" + result = subprocess.check_output(["vercmp", package.get_version(), aur_package.get_version()]) + result = int(result) + if result < 0: + return AURChecker.STATUS_NEEDS_UPDATE + elif result > 0: + return AURChecker.STATUS_NEEDS_DOWNGRADE + else: + return AURChecker.STATUS_UPTODATE + + + + +if __name__ == "__main__": + parser = argparse.ArgumentParser("Read local Arch Linux package databases/repositories and compare the version of each containing package against the AUR package with the same name to determine and print a status for each package.") + parser.add_argument('folder', help="source folder containing one or several databases/repositories (subdirectories possible)") + args = parser.parse_args() + AURChecker.check(args.folder) diff --git a/aur.py b/aur.py new file mode 100644 index 0000000..c46f4f6 --- /dev/null +++ b/aur.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + + +import certifi +import json +import urllib3 + + + + +class AURPackage: + """Representation of an Arch Linux User Repsitory (AUR) package.""" + """URL of the [AUR] website""" + AUR_URL = 'https://aur.archlinux.org' + """URL-path for packages""" + PACKAGE_PATH = 'packages' + """URL-path of RPC""" + RPC_PATH = 'rpc.php' + """Parameters of RPC""" + RPC_PARAMS = 'type=info&arg=' + + + def __init__(self, name): + """Construct a new AUR package representation.""" + self.name = name + self.version = None + self.desc = None + self.url = None + self.license = None + self.maintainer = None + self.votes = None + self.url_path = None + self.package_base = None + self.out_of_date = None + self.last_modified = None + self._load(name) + + + def _load(self, name): + """Load package via API.""" + # Construct URL + url = "{}/{}?{}{}".format(AURPackage.AUR_URL, AURPackage.RPC_PATH, AURPackage.RPC_PARAMS, name) + # Call API via https + https = urllib3.PoolManager(ca_certs=certifi.where()) + # Get and parse response + response = https.request('GET', url) + if response.status == 200: + data = json.loads(response.data.decode('utf-8')) + if data['resultcount'] > 0: + values = data['results'] + self.version = values['Version'] + self.desc = values['Description'] + self.url = values['URL'] + self.license = values['License'] + self.maintainer = values['Maintainer'] + self.votes = values['NumVotes'] + self.url_path = values['URLPath'] + self.package_base = values['PackageBase'] + self.out_of_date = values['OutOfDate'] + self.last_modified = values['LastModified'] + else: + print("error:", response.status) + + + def get_name(self): + """Get name.""" + return self.name + + + def get_version(self): + """Get version.""" + return self.version + + + def get_desc(self): + """Get description.""" + return self.desc + + + def get_url(self): + """Get URL.""" + return self.url + + + def get_license(self): + """Get license.""" + return self.license + + + def get_maintainer(self): + """Get maintainer.""" + return self.maintainer + + + def get_votes(self): + """Get number of votes.""" + return self.votes + + + def get_url_path(self): + """Get URL path.""" + return self.url_path + + + def get_package_base(self): + """Get package base.""" + return self.package_base + + + def get_out_of_date(self): + """Get date the package was flagged as out-of-date.""" + return self.out_of_date + + + def get_last_modified(self): + """Get date of last modification.""" + return self.last_modified