Blob Blame History Raw
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""This program fetches the latest version of the Lensfun database from the
Internet and places it on the local system.  This way, the user can update the
database conveniently.  Unfortunately, we have to take into account that the
Git database may have a too new format for the locally installed Lensfun.
Then, it may fetch backports of the database from other URLs.

This program must be called with root privileges.  It stores the new database
in `/var/lib/lensfun-updates`.

The repository of databases resides at a base URL.  Below that URL, there is
the file versions.json.  It contains a list with three elements.  The first is
the database timestamp, the second is a list of available version numbers, and
the third is a list of strings which represents further alternative base URLs
to look at.  So, the file may contain the following::

    [1386797501, [1, 2, 3], ["http://wilson.bronger.org/"]]

All URLs must end with a slash.  For every version number, there must be a file
called version_<versionnumber>.tar.bz2.  So in our case, there must be the
files

::

    version_1.tar.bz2
    version_2.tar.bz2
    version_3.tar.bz2

in the same directory as versions.json.  These tar balls contain the Lensfun
database with the given timestamp and version.

The timestamps are the number of seconds since the Epoch as an int.


Diagnostics:

Status code 0 -- successfully installed updates
            1 -- no newer upstream database found for last installed Lensfun
            3 -- no location was responsive; maybe network problems

"""

import urllib.request, shutil, sys, os, time, calendar, tarfile, json, glob
import lensfun


database_version = lensfun.get_database_version()

lensfun_updates_dir = "/var/lib/lensfun-updates"
if os.name == "posix" and os.geteuid() != 0:
    lensfun_updates_dir = os.path.join(os.path.expanduser("~"),
                                       ".local", "share", "lensfun", "updates")
    print("Info: root privileges needed for updating the system database.")
    print("Info: updating user DB in '%s'" % lensfun_updates_dir)


def seconds_since_epoch():
    return calendar.timegm(time.gmtime())


def detect_local_timestamp(version):
    if version == database_version:
        return lensfun.get_core_database()[0]

    def detect_single_timestamp(path):
        try:
            return int(open(os.path.join(path, "version_{}".format(version), "timestamp.txt")).read())
        except (FileNotFoundError, ValueError):
            return 0

    directory_candidates = {"/usr/share/lensfun", "/usr/local/share/lensfun",
                            "/var/lib/lensfun-updates", os.path.expanduser("~/.local/share/lensfun/updates")}
    return max(map(detect_single_timestamp, directory_candidates))


class Location:

    def __init__(self, base_url, version, timestamp):
        self.base_url, self.version, self.timestamp = base_url, version, timestamp

    def __lt__(self, other):
        return self.timestamp < other.timestamp

    def extract(self, directory):
        tar = tarfile.open(fileobj=urllib.request.urlopen(self.base_url + "version_{}.tar.bz2".format(self.version)),
                           mode="r|*")
        tar.extractall(directory)
        tar.close()


def detect_local_database_versions():
    versions = {database_version}
    for directory in glob.glob("/usr/share/lensfun/version_*") + glob.glob("/usr/local/share/lensfun/version_*"):
        if os.path.isdir(directory):
            try:
                versions.add(int(directory.partition("version_")[2]))
            except ValueError:
                pass
    return versions
local_database_versions = detect_local_database_versions()


locations = {version: set() for version in local_database_versions}
local_timestamps = {version: detect_local_timestamp(version) for version in local_database_versions}
seen_urls = set()
at_least_one_responsive_location = False

def read_location(base_url):
    global at_least_one_responsive_location
    if base_url not in seen_urls and len(seen_urls) < 50:
        seen_urls.add(base_url)
        print("Reading {} …".format(base_url + "versions.json"))
        try:
            response = urllib.request.urlopen(base_url + "versions.json")
        except (urllib.error.HTTPError, ValueError):
            print("  Error: URL could not be opened.")
        else:
            try:
                timestamp, versions, alternatives = json.loads(response.read().decode("utf-8"))
            except ValueError:
                print("  Error: Invalid data received.")
            else:
                at_least_one_responsive_location = True
                versions = set(versions) & set(locations)
                for version in versions:
                    if timestamp > local_timestamps[version]:
                        locations[version].add(Location(base_url, version, timestamp))
                for base_url in alternatives:
                    read_location(base_url)

read_location("http://lensfun.sourceforge.net/db/")
read_location("http://wilson.bronger.org/lensfun-db/")

if not at_least_one_responsive_location:
    print("Fatal: No location was responsive.  Network down?")
    sys.exit(3)
elif not locations[database_version]:
    print("Info: No newer database was found for last installed Lensfun.")
    sys.exit(1)


for version, location_list in locations.items():
    try:
        best_location = max(location_list)
    except ValueError:
        continue
    updates_dir = os.path.join(lensfun_updates_dir, "version_{}".format(version))
    shutil.rmtree(updates_dir, ignore_errors=True)
    os.makedirs(updates_dir)
    best_location.extract(updates_dir)
    open(os.path.join(updates_dir, "timestamp.txt"), "w").write(str(best_location.timestamp))
    print("Successfully updated the database in " + updates_dir + ".")