diff --git a/bin/isinstall b/bin/isinstall index b7008f35f56585e35d4376280d410a059cbac0bd..1ded51e28a135181419a729f289d8122ecf21823 100755 --- a/bin/isinstall +++ b/bin/isinstall @@ -13,7 +13,7 @@ import argparse import installsystems import installsystems.tools as istools from installsystems.printer import * -from installsystems.repository import RepositoryManager +from installsystems.repository import RepositoryManager, RepositoryConfig from installsystems.image import PackageImage from installsystems.config import ConfigFile @@ -34,12 +34,16 @@ p_main.add_argument('-d', "--debug", action=DebugAction, nargs=0, help="active debug mode") p_main.add_argument('-q', "--quiet", action="store_false", dest="verbose", default=True, help="active quiet mode") +p_main.add_argument("--no-cache", action="store_false", default=False, + help="Not use persistent db caching") p_main.add_argument("-c", "--config", dest="config", type=str, default=None, help="config file path") p_main.add_argument("-v", "--image-version", dest="image_version", type=int, default=None, help="image version") p_main.add_argument("-t", "--timeout", dest="timeout", type=int, default=None, help="download timeout") +p_main.add_argument("-r", "--repo", dest="repos", action="append", default=[], + help="repository (can be specified more than one time)") p_main.add_argument("image_name", type=str, help="image to install (path or name)") try: @@ -52,10 +56,17 @@ try: if image_name_type == "file": pkg = PackageImage(istools.abspath(args.image_name)) elif image_name_type == "name": + # remove cache is asked + if args.no_cache: + config.cache = None # init repo cache object - repoman = RepositoryManager(timeout=args.timeout, verbose=args.verbose) - # register repositories - repoman.register(config.repos) + repoman = RepositoryManager(config.cache, timeout=args.timeout, verbose=args.verbose) + # register config repositories + for crepo in config.repos: + repoman.register(crepo) + # register command line repositories + for rpath in args.repos: + repoman.register(RepositoryConfig(None, path=rpath)) # get image package pkg = repoman.get(args.image_name, args.image_version) else: diff --git a/bin/isrepo b/bin/isrepo index a4ef2b9d25b75ed9e1d5b5501db3c5434766c563..73c23de4963e3de054599afce9dfb3e7cb1076d3 100755 --- a/bin/isrepo +++ b/bin/isrepo @@ -26,24 +26,23 @@ def init(args): '''Create an empty fresh repo tree''' # call init from library try: - Repository.create(args.repository, args.verbose) + Repository.create(args.repository, verbose=args.verbose) except Exception as e: raise Exception("init failed: %s" % e) def add(args): '''Add a package to repository''' try: - repo = Repository(args.repository, args.verbose) - pkg = PackageImage(args.path, args.verbose) + repo = Repository(args.repository, verbose=args.verbose) + pkg = PackageImage(args.path, verbose=args.verbose) repo.add(pkg) except Exception as e: - raise raise Exception("add failed: %s" % e) def delete(args): '''Remove a package from repository''' try: - repo = Repository(args.repository, args.verbose) + repo = Repository(args.repository, verbose=args.verbose) repo.delete(args.image_name, args.image_version) except Exception as e: raise Exception("del failed: %s" % e) diff --git a/installsystems/config.py b/installsystems/config.py index d32a01bcd922a853aaa64e7cd3e7c11646260d50..9b75f8fc2c806cded976d20adf2449942ba900ef 100644 --- a/installsystems/config.py +++ b/installsystems/config.py @@ -58,7 +58,7 @@ class ConfigFile(object): def _cache_paths(self): '''List all candidates to cache directories. Alive or not''' - dirs = ["/var/tmp", "/tmp"] + dirs = [] # ["/var/tmp", "/tmp"] # we have a different behaviour if we are root dirs.insert(0, "/var/cache" if os.getuid() == 0 else os.path.expanduser("~/.cache")) return map(lambda x: os.path.join(x, self.prefix), dirs) diff --git a/installsystems/database.py b/installsystems/database.py index fd62e276de64cf529a10750b43917843c3be8591..597f4248eb2a99638497241d6ba278d43469b65f 100644 --- a/installsystems/database.py +++ b/installsystems/database.py @@ -11,12 +11,16 @@ import os import shutil import tarfile import cStringIO +import sqlite3 import installsystems.tools as istools +import installsystems.template as istemplate from installsystems.tarball import Tarball from installsystems.printer import * class Database(object): - '''Abstract repo database stuff''' + ''' Abstract repo database stuff + It needs to be local cause of sqlite3 which need to open a file + ''' db_format = "1" @@ -26,23 +30,27 @@ class Database(object): # check locality if istools.pathtype(path) != "file": raise NotImplementedError("Database creation must be local") - dbpath = istools.abspath(path) - if os.path.exists(dbpath): + path = os.path.abspath(path) + if os.path.exists(path): raise Exception("Database already exists. Remove it before") try: - tarball = Tarball.open(dbpath, mode="w:gz", dereference=True) - tarball.add_str("format", Database.db_format, tarfile.REGTYPE, 0444) - tarball.close() + conn = sqlite3.connect(path, isolation_level=None) + conn.execute("PRAGMA foreign_keys = ON") + conn.executescript(istemplate.createdb) + conn.commit() + conn.close() except Exception as e: raise Exception("Create database failed: %s" % e) return cls(path, verbose) def __init__(self, path, verbose=True): - self.path = istools.abspath(path) + # check locality + if istools.pathtype(path) != "file": + raise NotImplementedError("Database creation must be local") + self.path = os.path.abspath(path) self.verbose = verbose - # load db in memory - self.file = cStringIO.StringIO() - shutil.copyfileobj(istools.uopen(self.path), self.file) + self.conn = sqlite3.connect(self.path, isolation_level=None) + self.conn.execute("PRAGMA foreign_keys = ON") def get(self, name, version): '''Return a description dict from a package name''' @@ -55,44 +63,48 @@ class Database(object): except KeyError: raise Exception("No package %s version %s in metadata" % (name, version)) except Exception as e: - raise Exception("Unable to read db %s version %s: e" % (name, version, e)) + raise Exception("Unable to read db %s version %s: %s" % (name, version, e)) # convert loaded data into dict (json parser) try: return json.loads(rdata) except Exception as e: raise Exception("Invalid metadata in package %s version %s: e" % (name, version, e)) + def ask(self, sql, args=()): + '''Ask question to db''' + return self.conn.execute(sql, args) + def add(self, package): '''Add a packaged image to a db''' - arrow("Adding metadata to db", 1, self.verbose) - # check locality - if istools.pathtype(self.path) != "file": - raise NotImplementedError("Database addition must be local") - # naming - newdb_path = "%s.new" % self.path - # compute md5 - arrow("Formating metadata", 2, self.verbose) - desc = package.description - desc["md5"] = package.md5 - jdesc = json.dumps(desc) try: - arrow("Adding metadata", 2, self.verbose) - self.file.seek(0) - newfile = cStringIO.StringIO() - db = Tarball.open(fileobj=self.file, mode='r:gz') - newdb = Tarball.open(fileobj=newfile, mode='w:gz') - for ti in db.getmembers(): - if ti.name != package.id: - newdb.addfile(ti, db.extractfile(ti)) - newdb.add_str(package.id, jdesc, tarfile.REGTYPE, 0644) - db.close() - newdb.close() - # writing to disk - arrow("Writing to disk", 2, self.verbose) - self.file.close() - self.file = newfile - self.write() + # let's go + arrow("Begin transaction to db", 1, self.verbose) + self.conn.execute("BEGIN TRANSACTION") + # insert image information + arrow("Add image metadata", 2, self.verbose) + self.conn.execute("INSERT OR REPLACE INTO image values (?,?,?,?,?,?,?)", + (package.md5, + package.name, + package.version, + package.date, + package.author, + package.description, + package.size, + )) + # insert data informations + arrow("Add data metadata", 2, self.verbose) + for key,value in package.data.items(): + self.conn.execute("INSERT OR REPLACE INTO data values (?,?,?,?)", + (value["md5"], + package.md5, + key, + value["size"] + )) + # on commit + arrow("Commit transaction to db", 1, self.verbose) + self.conn.execute("COMMIT TRANSACTION") except Exception as e: + raise raise Exception("Adding metadata fail: %s" % e) def delete(self, name, version): @@ -148,14 +160,3 @@ class Database(object): if int(version) not in candidates: return None return self.get(name, version) - - def write(self): - '''Write current dabatase into its file''' - if istools.pathtype(self.path) != "file": - raise NotImplementedError("Database writing must be local") - try: - dest = open(self.path, "w") - self.file.seek(0) - shutil.copyfileobj(self.file, dest) - except Exception as e: - raise Exception("Unable to write database: %s" % e) diff --git a/installsystems/image.py b/installsystems/image.py index 1118f219585811d149cff1891efd2c7ace6f7f4f..5115b6c8b6233903ef3f2d653c572eb0b8762e7d 100644 --- a/installsystems/image.py +++ b/installsystems/image.py @@ -240,88 +240,94 @@ class SourceImage(Image): class PackageImage(Image): '''Packaged image manipulation class''' - def __init__(self, path, verbose=True): + def __init__(self, path, md5name=False, verbose=True): Image.__init__(self) self.path = istools.abspath(path) self.base_path = os.path.dirname(self.path) self.verbose = verbose + # tarball are named by md5 and not by real name + self.md5name = md5name # load image in memory arrow("Loading tarball in memory", 1, verbose) - self.file = cStringIO.StringIO() + memfile = cStringIO.StringIO() fo = istools.uopen(self.path) - shutil.copyfileobj(fo, self.file) + (self.size, self.md5) = istools.copyfileobj(fo, memfile) fo.close() # set tarball fo - self.file.seek(0) - self.tarball = Tarball.open(fileobj=self.file, mode='r:gz') - self.parse() - self._md5 = None + memfile.seek(0) + self._tarball = Tarball.open(fileobj=memfile, mode='r:gz') + self._metadata = self.read_metadata() - @property - def md5(self): - '''Return md5sum of the current tarball''' - if self._md5 is None: - self._md5 = istools.md5sum(self.path) - return self._md5 + def __getattr__(self, name): + """Give direct access to description field""" + if name in self._metadata: + return self._metadata[name] + raise AttributeError @property def id(self): '''Return image versionned name / id''' - return "%s-%s" % (self.description["name"], self.description["version"]) - - @property - def name(self): - '''Return image name''' - return self.description["name"] - - @property - def version(self): - '''Return image version''' - return self.description["version"] + return "%s-%s" % (self._metadata["name"], self._metadata["version"]) @property def filename(self): '''Return image filename''' return "%s%s" % (self.id, self.extension) - @property - def datas(self): - '''Create a dict of data tarballs''' - d = dict() - for dt in self.description["data"]: - d[dt] = dict(self.description["data"][dt]) - d[dt]["filename"] = "%s-%s%s" % (self.id, dt, self.extension_data) - d[dt]["path"] = os.path.join(self.base_path, d[dt]["filename"]) - return d - - def parse(self): - '''Parse tarball and extract metadata''' + def read_metadata(self): + '''Parse tarball and return metadata dict''' # extract metadata arrow("Read tarball metadata", 1, self.verbose) - img_format = self.tarball.get_str("format") - img_desc = self.tarball.get_str("description.json") + img_format = self._tarball.get_str("format") + img_desc = self._tarball.get_str("description.json") # check format - arrow("Read format", 2, self.verbose) + arrow("Read format file", 2, self.verbose) if img_format != self.format: raise Exception("Invalid tarball image format") # check description - arrow("Read description", 2, self.verbose) + arrow("Read description file", 2, self.verbose) try: - self.description = json.loads(img_desc) + desc = json.loads(img_desc) except Exception as e: raise Exception("Invalid description: %s" % e1) + # FIXME: we should check valid information here + return desc - def check_md5(self): - '''Check if md5 of data tarballs are correct''' - arrow("Check MD5", 1, self.verbose) - databalls = self.description["data"] - for databall in databalls: - md5_path = os.path.join(self.base_path, databall) - arrow(os.path.relpath(md5_path), 2, self.verbose) - md5_meta = databalls[databall]["md5"] - md5_file = istools.md5sum(md5_path) - if md5_meta != md5_file: - raise Exception("Invalid md5: %s" % databall) + @property + def tarballs(self): + '''List path of all related tarballs''' + d_d = {} + name = os.path.join(self.base_path, self.md5) if self.md5name else self.path + d_d[name] = {"md5": self.md5, "size": self.size} + for key, value in self._metadata["data"].items(): + if self.md5name: + name = os.path.join(self.base_path, value["md5"]) + else: + name = os.path.join(self.base_path, + "%s-%s%s" % (self.id, key, self.extension_data)) + d_d[name] = {"md5": value["md5"], "size": value["size"]} + return d_d + + def tarcheck(self, message="Check MD5"): + '''Check md5 and size of tarballs are correct''' + arrow(message, 1, self.verbose) + # open /dev/null + dn = open("/dev/null", "w") + for key,value in self.tarballs.items(): + arrow(os.path.basename(key), 2, self.verbose) + # open tarball + tfo = istools.uopen(key) + # compute sum and md5 using copy function + size, md5 = istools.copyfileobj(tfo ,dn) + # close tarball fo + tfo.close() + # check md5 + if md5 != value["md5"]: + raise Exception("Invalid md5: %s" % key) + # check size + if size != value["size"]: + raise Exception("Invalid size: %s" % key) + dn.close() def run_parser(self, gl): '''Run parser scripts''' @@ -336,14 +342,14 @@ class PackageImage(Image): '''Run scripts in a tarball directory''' arrow("Run %s" % directory, 1, self.verbose) # get list of parser scripts - l_scripts = self.tarball.getnames("%s/.*\.py" % directory) + l_scripts = self._tarball.getnames("%s/.*\.py" % directory) # order matter! l_scripts.sort() # run scripts for n_scripts in l_scripts: arrow(os.path.basename(n_scripts), 2, self.verbose) try: - s_scripts = self.tarball.get_str(n_scripts) + s_scripts = self._tarball.get_str(n_scripts) except Exception as e: raise Exception("Extracting script %s fail: %s" % (os.path.basename(n_scripts), e)) @@ -357,10 +363,10 @@ class PackageImage(Image): def extractdata(self, dataname, target, filelist=None): '''Extract a data tarball into target''' # check if dataname exists - if dataname not in self.description["data"].keys(): - raise Exception("No such data") + if dataname not in self._metadata["data"].keys(): + raise Exception("No such data: %s" % dataname) # tarball info - tinfo = self.description["data"][dataname] + tinfo = self._metadata["data"][dataname] # build data tar paths paths = [ os.path.join(self.base_path, tinfo["md5"]), os.path.join(self.base_path, "%s-%s%s" % (self.id, diff --git a/installsystems/repository.py b/installsystems/repository.py index 37a5b5053b5ea311527c2979a89a01ab6504c972..8f9d41953268cc7823b2743f76ea5c9ecc8ff881 100644 --- a/installsystems/repository.py +++ b/installsystems/repository.py @@ -12,6 +12,7 @@ import shutil import pwd import grp import copy +import tempfile import installsystems import installsystems.tools as istools from installsystems.printer import * @@ -20,12 +21,13 @@ from installsystems.image import Image, PackageImage from installsystems.database import Database class Repository(object): - '''Repository class''' + ''' Repository class + ''' def __init__(self, config, verbose=True): self.verbose = verbose self.config = config - self.db = Database(os.path.join(config.path, config.dbname), verbose=self.verbose) + self.db = Database(config.dbpath, verbose=self.verbose) @classmethod def create(cls, config, verbose=True): @@ -40,16 +42,15 @@ class Repository(object): if os.path.exists(config.path): arrow("%s already exists" % config.path, 2, verbose) else: - istools.mkdir(config.path, config.chown, config.chgroup, config.dchmod) + istools.mkdir(config.path, config.uid, config.gid, config.dmod) arrow("%s directory created" % config.path, 2, verbose) except Exception as e: raise Exception("Unable to create directory %s: %s" % (config.path, e)) # create database dbpath = os.path.join(config.path, config.dbname) d = Database.create(dbpath, verbose=verbose) - istools.chrights(dbpath, config.chown, config.chgroup, config.fchmod) + istools.chrights(dbpath, uid=config.uid, gid=config.gid, mode=config.fmod) # create last file - arrow("Creating last file", 1, verbose) self = cls(config, verbose) self.update_last() return self @@ -63,8 +64,7 @@ class Repository(object): arrow("Updating last file", 1, self.verbose) last_path = os.path.join(self.config.path, self.config.lastname) open(last_path, "w").write("%s\n" % int(time.time())) - os.chown(last_path, self.config.chown, self.config.chgroup) - os.chmod(last_path, self.config.fchmod) + istools.chrights(last_path, self.config.uid, self.config.gid, self.config.fmod) except Exception as e: raise Exception("Update last file failed: %s" % e) @@ -82,37 +82,25 @@ class Repository(object): # check local repository if istools.pathtype(self.config.path) != "file": raise NotImplementedError("Repository addition must be local") - # copy file to directory - arrow("MD5summing tarballs", 1, self.verbose) - # build dict of file to add - filelist = dict() - # script tarball - arrow(package.filename, 2, self.verbose) - filelist[package.md5] = package.path - # data tarballs - datas = package.datas - for dt in datas: - dt_path = datas[dt]["path"] - old_md5 = datas[dt]["md5"] - arrow(os.path.relpath(dt_path), 2, self.verbose) - md5 = istools.md5sum(dt_path) - if md5 != old_md5: - raise Exception("MD5 mismatch on %s" % dt_path) - filelist[md5] = dt_path + # checking data tarballs md5 before copy + package.tarcheck("Check tarballs before copy") # adding file to repository - arrow("Adding files to directory", 1, self.verbose) - for md5 in filelist: - dest = os.path.join(self.config.path, md5) - source = filelist[md5] + arrow("Copying files", 1, self.verbose) + for src,value in package.tarballs.items(): + dest = os.path.join(self.config.path, value["md5"]) + basesrc = os.path.basename(src) if os.path.exists(dest): - arrow("Skipping %s: already exists" % (os.path.basename(source)), - 2, self.verbose) + arrow("Skipping %s: already exists" % basesrc, 2, self.verbose) else: - arrow("Adding %s (%s)" % (os.path.basename(source), md5), 2, self.verbose) - istools.copy(source, dest, - self.config.chown, self.config.chgroup, self.config.fchmod) + arrow("Adding %s (%s)" % (basesrc, value["md5"]), 2, self.verbose) + istools.copy(src, dest, self.config.uid, self.config.gid, self.config.fmod) + # copy is done. create a package inside repo + r_package = PackageImage(os.path.join(self.config.path, package.md5), + md5name=True, verbose=self.verbose) + # checking data tarballs md5 after copy + r_package.tarcheck("Check tarballs after copy") # add description to db - self.db.add(package) + self.db.add(r_package) # update last file self.update_last() @@ -145,62 +133,55 @@ class Repository(object): arrow("Updating last file", 1, self.verbose) self.update_last() + def has(self, name, version): + return self.db.ask("select name,version from image where name = ? and version = ? limit 1", (name,version)).fetchone() is not None + def get(self, name, version): '''return a package from a name and version of pakage''' - desc = self.db.get(name, version) - p = PackageImage(os.path.join(self.config.path, desc["md5"]), verbose=self.verbose) - if p.md5 != desc["md5"]: - raise Exception("Invalid package MD5") - return p + debug("Getting %s v%s" % (name, version)) + # get file md5 from db + r = self.db.ask("select md5 from image where name = ? and version = ? limit 1", + (name,version)).fetchone() + if r is None: + raise Exception("No such image %s version %s" % name, version) + return PackageImage(os.path.join(self.config.path, r[0]), + md5name=True, + verbose=self.verbose) + + def last(self, name): + '''Return last version of name in repo or -1 if not found''' + r = self.db.ask("select version from image where name = ? order by version desc limit 1", (name,)).fetchone() + # no row => no way + if r is None: + return -1 + # return last + return r[0] + class RepositoryConfig(object): '''Repository configuration container''' - def __init__(self, *args, **kwargs): + def __init__(self, name, **kwargs): # set default value for arguments - self.name = args[0] + self.name = name + self.path = "" + self._dbpath = None self.dbname = "db" + self._lastpath = None self.lastname = "last" - self.path = "" - self.chown = os.getuid() - self.chgroup = os.getgid() + self._uid = os.getuid() + self._gid = os.getgid() umask = os.umask(0) os.umask(umask) - self.fchmod = 0666 & ~umask - self.dchmod = 0777 & ~umask + self._fmod = 0666 & ~umask + self._dmod = 0777 & ~umask self.update(**kwargs) - def update(self, *args, **kwargs): - '''Update attribute with checking value - All attribute must already exists - ''' - # autoset parameter in cmdline - for k in kwargs: - if hasattr(self, k): - # attribute which are not in the following list cannot be loaded - # from configuration - try: - # convert userid - if k == "chown": - if not k.isdigit(): - kwargs[k] = pwd.getpwnam(kwargs[k]).pw_uid - setattr(self, k, int(kwargs[k])) - # convert gid - elif k == "chgroup": - if not k.isdigit(): - kwargs[k] = grp.getgrnam(kwargs[k]).gr_gid - setattr(self, k, int(kwargs[k])) - # convert file mode - elif k in ("fchmod", "dchmod"): - setattr(self, k, int(kwargs[k], 8)) - # convert repo path - elif k in ("image", "data"): - setattr(self, k, istools.abspath(kwargs[k])) - # else is string - else: - setattr(self, k, kwargs[k]) - except Exception as e: - warn("Unable to set config parameter %s in repository %s: %s" % (k, self.name, e)) + def __str__(self): + l = [] + for a in ("name", "path", "dbpath", "lastpath", "uid", "gid", "fmod", "dmod"): + l.append("%s: %s" % (a, getattr(self, a))) + return os.linesep.join(l) def __eq__(self, other): return vars(self) == vars(other) @@ -211,150 +192,179 @@ class RepositoryConfig(object): def __contains__(self, key): return key in self.__dict__ -class RepositoryManager(object): - '''Manage multiple repostories''' + @property + def lastpath(self): + """return the last file complete path""" + if self._lastpath is None: + return os.path.join(self.path, self.lastname) + return self._lastpath - def __init__(self, timeout=None, verbose=True): - self.verbose = verbose - self.timeout = 3 if timeout is None else timeout - self.repos = {} + @lastpath.setter + def lastpath(self, value): + '''Set last path''' + self._lastpath = value - def register(self, configs): - '''Register a list of repository from its config''' - for conf in configs: - self.repos[conf.name] = Repository(conf, self.verbose) + @property + def dbpath(self): + """return the db complete path""" + if self._dbpath is None: + return os.path.join(self.path, self.dbname) + return self._dbpath - def find_image(self, name, version): - '''Find a repository containing image''' - if version is None: - arrow("Searching last version of %s" % name, 1, self.verbose) + @dbpath.setter + def dbpath(self, value): + '''Set db path''' + # dbpath must be local, sqlite3 requirment + if istools.pathtype(value) != "file": + raise ValueError("Database path must be local") + self._dbpath = os.path.abspath(value) + + @property + def uid(self): + '''Return owner of repository''' + return self._uid + + @uid.setter + def uid(self, value): + '''Define user name owning repository''' + if not value.isdigit(): + self._uid = pwd.getpwnam(value).pw_uid else: - arrow("Searching %s version %s " % (name, version), 1, self.verbose) - img = None - # search in all repositories - desc = None - for repo in self.repos: - desc = self.repos[repo].db.find(name, version) - if desc is not None: - # \o/ - break - if desc is None: - arrow("Not found", 2, self.verbose) - if version is None: - error("Unable to find a version of image %s" % name) + self._uid = int(value) + + @property + def gid(self): + '''Return group of the repository''' + return self._gid + + @gid.setter + def gid(self, value): + '''Define group owning repository''' + if not value.isdigit(): + self._gid = grp.getgrnam(value).gr_gid + else: + self._gid = int(value) + + @property + def fmod(self): + '''Return new file mode''' + return self._fmod + + @fmod.setter + def fmod(self, value): + '''Define new file mode''' + if value.isdigit(): + self._fmod = int(value, 8) + else: + raise ValueError("File mode must be an integer") + + @property + def dmod(self): + '''Return new directory mode''' + return self._dmod + + @dmod.setter + def dmod(self, value): + '''Define new directory mode''' + if value.isdigit(): + self._dmod = int(value, 8) + else: + raise ValueError("Directory mode must be an integer") + + def update(self, *args, **kwargs): + '''Update attribute with checking value + All attribute must already exists + ''' + # autoset parameter in cmdline + for k in kwargs: + if hasattr(self, k): + try: + setattr(self, k, kwargs[k]) + except Exception as e: + warn("Unable to set config parameter %s in repository %s: %s" % (k, self.name, e)) else: - error("Unable to find image %s version %s" % (name, version)) - arrow("Found %s version %s " % (desc["name"], desc["version"]), 2, self.verbose) - return (desc, self.repos[repo]) + debug("No such repository parameter: %s" % k) - def get(self, name, version=None): - '''Return a package object from local cache''' - # find an image name/version in repository - (desc, repo) = self.find_image(name, version) - # get pkg object - return repo.get(desc["name"], desc["version"]) -class RepositoryCache(object): - '''Local repository cache class''' +class RepositoryManager(object): + ''' + Manage multiple repostories + + This call implement a cache and a manager for multiple repositories + ''' - def __init__(self, cache_path, timeout=3, verbose=True): + def __init__(self, cache_path=None, timeout=None, verbose=True): self.verbose = verbose - self.timeout = timeout - self.repos = {} - self.path = os.path.abspath(cache_path) - # ensure cache directories are avaiblable - if not os.path.exists(self.path): - os.mkdir(self.path) - if not os.access(self.path, os.W_OK | os.X_OK): - raise Exception("%s is not writable or executable" % path) - debug("Repository cache is in %s" % self.path) - - def register(self, configs): - '''Register a list of repository from its config''' - for conf in configs: - self.repos[conf.name] = {} - # keep original repository conf - self.repos[conf.name]["orig"] = Repository(conf, self.verbose) - # change configuration to make remote repository in cache - cconf = copy.copy(conf) - cconf.image = os.path.join(self.path, conf.name) - cconf.data = "/dev/null" - self.repos[conf.name]["cache"] = Repository(cconf, self.verbose) - # create a local directory - if not os.path.exists(cconf.image): - os.mkdir(cconf.image) - - def update(self): - '''Update cache info''' - arrow("Updating repositories", 1, self.verbose) - for r in self.repos: - # last local - local_last = self.last(r) - # copy last file - arrow("Checking %s repository last" % r, 2, self.verbose) - istools.copy(self.repos[r]["orig"].last_path, - self.repos[r]["cache"].last_path, timeout=self.timeout) - # last after update - remote_last = self.last(r) - debug("%s: last: local: %s, remote:%s" % (r, local_last, remote_last)) - # Updating db? - remote_db = self.repos[r]["orig"].db.path - local_db = self.repos[r]["cache"].db.path - if remote_last > local_last or not os.path.exists(local_db): - # copy db file - arrow("Copying %s repository db" % r, 2, self.verbose) - istools.copy(remote_db, local_db, timeout=self.timeout) - - def last(self, reponame): - '''Return the last timestamp of a repo''' - last_path = os.path.join(self.path, reponame, "last") - try: - return int(open(last_path, "r").read().rstrip()) - except Exception: - return 0 - - def get_image(self, reponame, imagename, imageversion): - '''Obtain a local path in cache for a remote image in repo''' - arrow("Getting image", 1, self.verbose) - filename = "%s-%s%s" % (imagename, imageversion, Image.image_extension) - cachepath = os.path.join(self.repos[reponame]["cache"].config.image, filename) - # return db path if exists - if os.path.exists(cachepath): - arrow("Found in cache", 2, self.verbose) + self.timeout = 3 if timeout is None else timeout + self.repos = [] + self.tempfiles = [] + if cache_path is None: + self.cache_path = None + debug("No repository cache") else: - # get remote image - remotepath = os.path.join(self.repos[reponame]["orig"].config.image, filename) - arrow("Copying from repository", 2, self.verbose) - istools.copy(remotepath, cachepath, timeout=self.timeout) - return cachepath - - def find_image(self, name, version): - '''Find an image in repositories''' - if version is None: - arrow("Searching last version of %s" % name, 1, self.verbose) + if istools.pathtype(cache_path) != "file": + raise NotImplementedError("Repository cache must be local") + self.cache_path = os.path.abspath(cache_path) + # must_path is a list of directory which must exists + # create directory if not exists + if not os.path.exists(self.cache_path): + os.mkdir(self.cache_path) + # ensure directories are avaiblable + if not os.access(self.cache_path, os.W_OK | os.X_OK): + raise Exception("%s is not writable or executable" % t_path) + debug("Repository cache is in %s" % self.cache_path) + + def __del__(self): + # delete temporary files (used by db) + for f in self.tempfiles: + try: + debug("Removing %s" % f) + os.unlink(f) + except OSError: + pass + + def register(self, config): + '''Register a repository from its config''' + debug("Registering repository %s (%s)" % (config.path, config.name)) + # find destination file and load last info + if config.name is None or self.cache_path is None: + # this is a forced temporary repository or without name repo + tempfd, filedest = tempfile.mkstemp() + os.close(tempfd) + self.tempfiles.append(filedest) else: - arrow("Searching %s version %s " % (name, version), 1, self.verbose) - img = None - # search in all repositories - for repo in self.repos: - tempdb = Database(self.repos[repo]["cache"].db.path, False) - img = tempdb.find(name, version) - if img is not None: - # \o/ - break - if img is None: - arrow("Not found", 2, self.verbose) - if version is None: - error("Unable to find a version of image %s" % name) - else: - error("Unable to find image %s version %s" % (name, version)) - arrow("Found %s version %s " % (img[0], img[1]), 2, self.verbose) - return (repo, img[0], img[1]) + filedest = os.path.join(self.cache_path, config.name) + # create file if not exists + if not os.path.exists(filedest): + open(filedest, "wb") + # get remote last value + rlast = int(istools.uopen(config.lastpath).read().strip()) + # get local last value + llast = int(os.stat(filedest).st_mtime) + # if repo is out of date, download it + if rlast != llast: + arrow("Getting %s" % config.dbpath, 1, self.verbose) + istools.copy(config.dbpath, filedest, + uid=config.uid, + gid=config.gid, + mode=config.fmod, + timeout=self.timeout) + os.utime(filedest, (rlast, rlast)) + config.dbpath = filedest + self.repos.append(Repository(config, self.verbose)) def get(self, name, version=None): - '''Return a package object from local cache''' - r, n, v = self.find_image(name, version) - # download image if not in cache - path = self.get_image(r, n, v) - # create an object image - return PackageImage(path, self.verbose) + '''Crawl all repo to get the most recent image''' + # search last version if needed + if version is None: + lv = -1 + for repo in self.repos: + lv = max(lv, repo.last(name)) + if lv < 0: + raise Exception("Unable to find last version of %s" % name) + version = lv + # search image in repos + for repo in self.repos: + if repo.has(name, version): + return repo.get(name, version) + raise Exception("Unable to find %s v%s" % (name, version)) diff --git a/installsystems/template.py b/installsystems/template.py index 61f57aaa3d44c93516404e0ce83cae22fe906215..748ffd774e02698d1cb51573654e337b890dece8 100644 --- a/installsystems/template.py +++ b/installsystems/template.py @@ -28,3 +28,20 @@ image.extractdata("rootfs", args.target) # vim:set ts=2 sw=2 noet: """ + +createdb = """ +CREATE TABLE image (md5 TEXT NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + version INTEGER NOT NULL, + date INTEGER NOT NULL, + author TEXT, + description TEXT, + size INTEGER NOT NULL, + UNIQUE(name, version)); + +CREATE TABLE data (md5 TEXT NOT NULL, + image_md5 TEXT NOT NULL REFERENCES image(md5), + name TEXT NOT NULL, + size INTEGER NOT NULL, + PRIMARY KEY(md5, image_md5)); +""" diff --git a/installsystems/tools.py b/installsystems/tools.py index 7a3fb8258d4e0d6d016417cfb3acfc1cb7ab74b0..f42f85c78a1cfe3bc012c25d0788d0ca970b9138 100644 --- a/installsystems/tools.py +++ b/installsystems/tools.py @@ -26,6 +26,20 @@ def md5sum(path=None, fileobj=None): m.update(buf) return m.hexdigest() +def copyfileobj(sfile, dfile): + """Copy data from sfile to dfile coputing length and md5 on the fly""" + f_sum = hashlib.md5() + f_len = 0 + while True: + buf = sfile.read(1024 * f_sum.block_size) + buf_len = len(buf) + if buf_len == 0: + break + f_len += buf_len + f_sum.update(buf) + dfile.write(buf) + return (f_len , f_sum.hexdigest()) + def copy(source, destination, uid=None, gid=None, mode=None, timeout=None): '''Copy a source to destination. Take care of path type''' stype = pathtype(source)