From: Michael Tremer Date: Sat, 19 Feb 2011 16:58:10 +0000 (+0100) Subject: Improve repository handling. X-Git-Tag: 0.9.3~168 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=66af936c63180619742efc191d79242048e3f249;p=pakfire.git Improve repository handling. There are changes on code, that handles the internal and remote database. --- diff --git a/pakfire/__init__.py b/pakfire/__init__.py index 543b097fb..8ed7801a6 100644 --- a/pakfire/__init__.py +++ b/pakfire/__init__.py @@ -179,18 +179,17 @@ class Pakfire(object): return pkgs - def repo_create(self, path): + def repo_create(self, path, input_paths): if not os.path.exists(path) or not os.path.isdir(path): raise PakfireError, "Given path is not existant or not a directory: %s" % path - repo = repository.RemoteRepository( + repo = repository.LocalRepository( self, name="new", description="New repository.", - url="file://%s" % path, - gpgkey="XXX", - enabled=True, + path=path, ) - repo.save_index() + for input_path in input_paths: + repo._collect_packages(input_path) diff --git a/pakfire/cli.py b/pakfire/cli.py index dba347f07..158417c12 100644 --- a/pakfire/cli.py +++ b/pakfire/cli.py @@ -279,6 +279,7 @@ class CliBuilder(Cli): sub_create = sub_commands.add_parser("create", help=_("Create a new repository index.")) sub_create.add_argument("path", nargs=1, help=_("Path to the packages.")) + sub_create.add_argument("inputs", nargs="+", help=_("Path to input packages.")) sub_create.add_argument("action", action="store_const", const="repo_create") def handle_build(self): @@ -346,5 +347,5 @@ class CliBuilder(Cli): def handle_repo_create(self): path = self.args.path[0] - self.pakfire.repo_create(path) + self.pakfire.repo_create(path, self.args.inputs) diff --git a/pakfire/constants.py b/pakfire/constants.py index fef8ba698..9963c950e 100644 --- a/pakfire/constants.py +++ b/pakfire/constants.py @@ -14,7 +14,8 @@ CCACHE_CACHE_DIR = os.path.join(CACHE_DIR, "ccache") LOCAL_BUILD_REPO_PATH = "/var/lib/pakfire/local" -PACKAGES_DB = "var/lib/pakfire/packages.db" +PACKAGES_DB_DIR = "var/lib/pakfire" +PACKAGES_DB = os.path.join(PACKAGES_DB_DIR, "packages.db") REPOSITORY_DB = "index.db" BUFFER_SIZE = 1024**2 diff --git a/pakfire/database.py b/pakfire/database.py index 4c1a1c610..0531db577 100644 --- a/pakfire/database.py +++ b/pakfire/database.py @@ -3,9 +3,12 @@ import logging import os import sqlite3 +import time import packages +from constants import * + class Database(object): def __init__(self, pakfire, filename): self.pakfire = pakfire @@ -60,9 +63,7 @@ class PackageDatabase(Database): pkg INTEGER, size INTEGER, type INTEGER, - hash1 TEXT, - installed INTEGER, - changed INTEGER + hash1 TEXT ); CREATE TABLE packages( @@ -72,9 +73,6 @@ class PackageDatabase(Database): version TEXT, release TEXT, filename TEXT, - installed INTEGER, - reason TEXT, - repository TEXT, hash1 TEXT, provides TEXT, requires TEXT, @@ -115,18 +113,27 @@ class PackageDatabase(Database): for i in c: ret = i["id"] break - + c.close() return ret def add_package(self, pkg): + raise NotImplementedError + + +class RemotePackageDatabase(PackageDatabase): + def add_package(self, pkg, reason=None): if self.package_exists(pkg): logging.debug("Skipping package which already exists in database: %s" % pkg.friendly_name) return logging.debug("Adding package to database: %s" % pkg.friendly_name) + filename = "" + if pkg.repo.local: + filename = pkg.filename[len(pkg.repo.path) + 1:] + c = self.cursor() c.execute(""" INSERT INTO packages( @@ -135,21 +142,39 @@ class PackageDatabase(Database): version, release, filename, + hash1, provides, - requires - ) VALUES(?, ?, ?, ?, ?, ?, ?)""", + requires, + conflicts, + obsoletes, + license, + summary, + description, + build_id, + build_host, + build_date + ) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", ( pkg.name, pkg.epoch, pkg.version, pkg.release, - pkg.filename, + filename, + pkg.hash1, " ".join(pkg.provides), " ".join(pkg.requires), + " ".join(pkg.conflicts), + " ".join(pkg.obsoletes), + pkg.license, + pkg.summary, + pkg.description, + pkg.build_id, + pkg.build_host, + pkg.build_date ) ) - c.close() self.commit() + c.close() pkg_id = self.get_id_by_pkg(pkg) @@ -157,44 +182,52 @@ class PackageDatabase(Database): for file in pkg.filelist: c.execute("INSERT INTO files(name, pkg) VALUES(?, ?)", (file, pkg_id)) + self.commit() c.close() + + return pkg_id + + +class LocalPackageDatabase(RemotePackageDatabase): + def __init__(self, pakfire): + # Generate filename for package database + filename = os.path.join(pakfire.path, PACKAGES_DB) + + RemotePackageDatabase.__init__(self, pakfire, filename) + + def create(self): + RemotePackageDatabase.create(self) + + # Alter the database layout to store additional local information. + logging.debug("Altering database table for local information.") + c = self.cursor() + c.executescript(""" + ALTER TABLE packages ADD COLUMN installed INT; + ALTER TABLE packages ADD COLUMN reason TEXT; + ALTER TABLE packages ADD COLUMN repository TEXT; + """) self.commit() + c.close() + def add_package(self, pkg, reason=None): + # Insert all the information to the database we have in the remote database + pkg_id = RemotePackageDatabase.add_package(self, pkg) -class LocalPackageDatabase(PackageDatabase): - def add_package(self, pkg, installed=True): + # then: add some more information c = self.cursor() - c.execute("INSERT INTO packages(name, epoch, version, release, installed, \ - provides, requires, build_id, build_host, build_date) \ - VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", ( - pkg.name, - pkg.epoch, - pkg.version, - pkg.release, - int(installed), - " ".join(pkg.provides), - " ".join(pkg.requires), - pkg.build_id, - pkg.build_host, - pkg.build_date - )) - - #c.close() - - # Get the id from the package - #c = self.cursor() - #c.execute("SELECT * FROM packages WHERE build_id = ? LIMIT 1", (pkg.build_id)) - c.execute("SELECT * FROM packages WHERE name = ? AND version = ? AND \ - release = ? AND epoch = ? LIMIT 1", (pkg.name, pkg.version, pkg.release, pkg.epoch)) + # Save timestamp when the package was installed. + c.execute("UPDATE packages SET installed = ? WHERE id = ?", (time.time(), pkg_id)) - ret = None - for pkg in c: - ret = packages.InstalledPackage(self.pakfire, self, pkg) - break + # Add repository information. + c.execute("UPDATE packages SET repository = ? WHERE id = ?", (pkg.repo.name, pkg_id)) - assert ret - c.close() + # Save reason of installation (if any). + if reason: + c.execute("UPDATE packages SET reason = ? WHERE id = ?", (reason, pkg_id)) - return ret + # Update the filename information. + c.execute("UPDATE packages SET filename = ? WHERE id = ?", (pkg.filename, pkg_id)) + self.commit() + c.close() diff --git a/pakfire/index.py b/pakfire/index.py index 757d7a47c..63f698ac1 100644 --- a/pakfire/index.py +++ b/pakfire/index.py @@ -5,6 +5,7 @@ import os import database import packages +import repository from constants import * @@ -55,9 +56,14 @@ class Index(object): def update(self, force=False): raise NotImplementedError + def add_package(self, pkg): + raise NotImplementedError + class DirectoryIndex(Index): def __init__(self, pakfire, repo, path): + if path.startswith("file://"): + path = path[7:] self.path = path Index.__init__(self, pakfire, repo) @@ -108,11 +114,23 @@ class DirectoryIndex(Index): class DatabaseIndex(Index): - def __init__(self, pakfire, repo, db): - self.db = db - + def __init__(self, pakfire, repo): Index.__init__(self, pakfire, repo) + self.db = None + + if isinstance(repo, repository.InstalledRepository): + self.db = database.LocalPackageDatabase(self.pakfire) + + else: + # Generate path to database file. + filename = os.path.join(repo.path, "XXX-to-be-renamed.db") + self.db = database.RemotePackageDatabase(self.pakfire, filename) + + @property + def local(self): + pass + def update(self, force=False): """ Nothing to do here. @@ -148,6 +166,9 @@ class DatabaseIndex(Index): c.close() + def add_package(self, pkg, reason=None): + return self.db.add_package(pkg, reason) + # XXX maybe this can be removed later? class InstalledIndex(DatabaseIndex): diff --git a/pakfire/packages/binary.py b/pakfire/packages/binary.py index 51fcdaf8b..cb3273795 100644 --- a/pakfire/packages/binary.py +++ b/pakfire/packages/binary.py @@ -28,6 +28,14 @@ class BinaryPackage(FilePackage): def provides(self): return self.metadata.get("PKG_PROVIDES").split() + @property + def conflicts(self): + return self.metadata.get("PKG_CONFLICTS", "").split() + + @property + def obsoletes(self): + return self.metadata.get("PKG_OBSOLETES", "").split() + def get_extractor(self, pakfire): return packager.Extractor(pakfire, self) diff --git a/pakfire/packages/file.py b/pakfire/packages/file.py index 6ca78ad9e..7a1257a34 100644 --- a/pakfire/packages/file.py +++ b/pakfire/packages/file.py @@ -4,6 +4,8 @@ import tarfile import os import re +import util + from pakfire.errors import FileError from base import Package @@ -160,3 +162,10 @@ class FilePackage(Package): return ret or None + @property + def hash1(self): + """ + Calculate the hash1 of this package. + """ + return util.calc_hash1(self.filename) + diff --git a/pakfire/packages/installed.py b/pakfire/packages/installed.py index 05c43a811..54381bdd5 100644 --- a/pakfire/packages/installed.py +++ b/pakfire/packages/installed.py @@ -1,10 +1,5 @@ #!/usr/bin/python -import hashlib -import time - -import util - from base import Package class DatabasePackage(Package): @@ -130,26 +125,6 @@ class DatabasePackage(Package): c.close() - ## database methods - - def set_installed(self, installed): - c = self.db.cursor() - c.execute("UPDATE packages SET installed = ? WHERE id = ?", (installed, self.id)) - c.close() - - def add_file(self, filename, type=None, size=None, hash1=None, **kwargs): - if not hash1: - hash1 = util.calc_hash1(filename) - - if size is None: - size = os.path.getsize(filename) - - c = self.db.cursor() - c.execute("INSERT INTO files(name, pkg, size, type, hash1, installed) \ - VALUES(?, ?, ?, ?, ?, ?)", - (filename, self.id, size, type, hash1, time.time())) - c.close() - # XXX maybe we can remove this later? class InstalledPackage(DatabasePackage): diff --git a/pakfire/repository.py b/pakfire/repository.py index e17d72d9c..23688a758 100644 --- a/pakfire/repository.py +++ b/pakfire/repository.py @@ -13,6 +13,7 @@ from urlgrabber.progress import TextMultiFileMeter import base import database import index +import packages from constants import * @@ -33,7 +34,7 @@ class Repositories(object): self._repos = [] # Create the local repository - self.local = LocalRepository(self.pakfire) + self.local = InstalledRepository(self.pakfire) self.add_repo(self.local) # If we running in build mode, we include our local build repository. @@ -149,6 +150,15 @@ class RepositoryFactory(object): def priority(self): raise NotImplementedError + @property + def local(self): + """ + Say if a repository is a local one or remotely located. + + Used to check if we need to download files. + """ + return False + def update_index(self, force=False): """ A function that is called to update the local data of @@ -226,24 +236,94 @@ class FileSystemRepository(RepositoryFactory): class LocalRepository(RepositoryFactory): - def __init__(self, pakfire): - RepositoryFactory.__init__(self, pakfire, "installed", "Installed packages") + def __init__(self, pakfire, name, description, path): + RepositoryFactory.__init__(self, pakfire, name, description) - self.path = os.path.join(self.pakfire.path, PACKAGES_DB) + # Save location of the repository + self.path = path - self.db = database.LocalPackageDatabase(self.pakfire, self.path) + self.index = index.DatabaseIndex(self.pakfire, self) - self.index = index.InstalledIndex(self.pakfire, self, self.db) + @property + def local(self): + # This is obviously local. + return True @property def priority(self): """ - The local repository has always the highest priority. + The local repository has always a high priority. """ - return 0 + return 10 # XXX need to implement better get_by_name + def _collect_packages(self, path): + logging.info("Collecting packages from %s." % path) + + for dir, subdirs, files in os.walk(path): + for file in files: + if not file.endswith(".%s" % PACKAGE_EXTENSION): + continue + + file = os.path.join(dir, file) + + pkg = packages.BinaryPackage(self.pakfire, self, file) + self._add_package(pkg) + + def _add_package(self, pkg): + # XXX gets an instance of binary package and puts it into the + # repo location if not done yet + # then: the package gets added to the index + + if not isinstance(pkg, packages.BinaryPackage): + raise Exception + + repo_filename = os.path.join(self.path, pkg.arch, os.path.basename(pkg.filename)) + + pkg_exists = None + if os.path.exists(repo_filename): + pkg_exists = packages.BinaryPackage(self.pakfire, self, repo_filename) + + # If package in the repo is equivalent to the given one, we can + # skip any further processing. + if pkg == pkg_exists: + logging.debug("The package does already exist in this repo: %s" % pkg.friendly_name) + return + + logging.debug("Copying package '%s' to repository." % pkg.friendly_name) + repo_dirname = os.path.dirname(repo_filename) + if not os.path.exists(repo_dirname): + os.makedirs(repo_dirname) + + os.link(pkg.filename, repo_filename) + + # Create new package object, that is connected to this repository + # and so we can do stuff. + pkg = packages.BinaryPackage(self.pakfire, self, repo_filename) + + logging.info("Adding package '%s' to repository." % pkg.friendly_name) + self.index.add_package(pkg) + + +class InstalledRepository(RepositoryFactory): + def __init__(self, pakfire): + RepositoryFactory.__init__(self, pakfire, "installed", "Installed packages") + + self.index = index.InstalledIndex(self.pakfire, self) + + @property + def local(self): + # This is obviously local. + return True + + @property + def priority(self): + """ + The installed repository has always the highest priority. + """ + return 0 + class LocalBuildRepository(LocalRepository): def __init__(self, pakfire): @@ -271,11 +351,10 @@ class RemoteRepository(RepositoryFactory): else: self.enabled = False - if self.url.startswith("file://"): - self.index = index.DirectoryIndex(self.pakfire, self, self.url[7:]) - + if self.local: + self.index = index.DirectoryIndex(self.pakfire, self, self.url) else: - self.index = None + self.index = index.DatabaseIndex(self.pakfire, self) logging.debug("Created new repository(name='%s', url='%s', enabled='%s')" % \ (self.name, self.url, self.enabled)) @@ -283,6 +362,23 @@ class RemoteRepository(RepositoryFactory): def __repr__(self): return "<%s %s>" % (self.__class__.__name__, self.url) + @property + def local(self): + # If files are located somewhere in the filesystem we assume it is + # local. + if self.url.startswith("file://"): + return True + + # Otherwise not. + return False + + @property + def path(self): + if self.local: + return self.url[7:] + + raise Exception, "XXX find some cache dir" + @property def priority(self): priority = 100 diff --git a/pakfire/transaction.py b/pakfire/transaction.py index f5f42c142..b3100546d 100644 --- a/pakfire/transaction.py +++ b/pakfire/transaction.py @@ -48,17 +48,13 @@ class ActionExtract(Action): logging.debug("Extracting package %s" % self.pkg.friendly_name) # Create package in the database - virtpkg = self.local.db.add_package(self.pkg, installed=False) + virtpkg = self.local.index.add_package(self.pkg) # Grab an instance of the extractor and set it up extractor = self.pkg.get_extractor(self.pakfire) # Extract all files to instroot - extractor.extractall(self.pakfire.path, callback=virtpkg.add_file) - - # Mark package as installed - virtpkg.set_installed(True) - #self.db.commit() + extractor.extractall(self.pakfire.path) # Remove all temporary files extractor.cleanup() diff --git a/po/pakfire.pot b/po/pakfire.pot index 149feb511..846235c01 100644 --- a/po/pakfire.pot +++ b/po/pakfire.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2011-02-06 15:13+0100\n" +"POT-Creation-Date: 2011-02-19 17:57+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,191 +17,229 @@ msgstr "" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" -#: ../pakfire/cli.py:25 +#: ../pakfire/cli.py:26 #, python-format msgid "%s [y/N]" msgstr "" -#: ../pakfire/cli.py:36 +#: ../pakfire/cli.py:37 msgid "Pakfire command line interface." msgstr "" -#: ../pakfire/cli.py:43 +#: ../pakfire/cli.py:44 msgid "The path where pakfire should operate in." msgstr "" -#: ../pakfire/cli.py:72 +#: ../pakfire/cli.py:77 msgid "Enable verbose output." msgstr "" -#: ../pakfire/cli.py:75 +#: ../pakfire/cli.py:80 msgid "Path to a configuration file to load." msgstr "" -#: ../pakfire/cli.py:78 +#: ../pakfire/cli.py:83 msgid "Disable a repository temporarily." msgstr "" -#: ../pakfire/cli.py:83 +#: ../pakfire/cli.py:88 msgid "Install one or more packages to the system." msgstr "" -#: ../pakfire/cli.py:85 +#: ../pakfire/cli.py:90 msgid "Give name of at least one package to install." msgstr "" -#: ../pakfire/cli.py:91 +#: ../pakfire/cli.py:96 +msgid "Install one or more packages from the filesystem." +msgstr "" + +#: ../pakfire/cli.py:98 +msgid "Give filename of at least one package." +msgstr "" + +#: ../pakfire/cli.py:104 msgid "Update the whole system or one specific package." msgstr "" -#: ../pakfire/cli.py:93 +#: ../pakfire/cli.py:106 msgid "Give a name of a package to update or leave emtpy for all." msgstr "" -#: ../pakfire/cli.py:99 +#: ../pakfire/cli.py:112 msgid "Print some information about the given package(s)." msgstr "" -#: ../pakfire/cli.py:101 +#: ../pakfire/cli.py:114 msgid "Give at least the name of one package." msgstr "" -#: ../pakfire/cli.py:107 +#: ../pakfire/cli.py:120 msgid "Search for a given pattern." msgstr "" -#: ../pakfire/cli.py:109 +#: ../pakfire/cli.py:122 msgid "A pattern to search for." msgstr "" -#: ../pakfire/cli.py:149 +#: ../pakfire/cli.py:128 +msgid "Get a list of packages that provide a given file or feature." +msgstr "" + +#: ../pakfire/cli.py:130 +msgid "File or feature to search for." +msgstr "" + +#: ../pakfire/cli.py:192 msgid "Pakfire builder command line interface." msgstr "" -#: ../pakfire/cli.py:185 +#: ../pakfire/cli.py:232 msgid "Update the package indexes." msgstr "" -#: ../pakfire/cli.py:191 +#: ../pakfire/cli.py:238 msgid "Build one or more packages." msgstr "" -#: ../pakfire/cli.py:193 +#: ../pakfire/cli.py:240 msgid "Give name of at least one package to build." msgstr "" -#: ../pakfire/cli.py:197 +#: ../pakfire/cli.py:244 msgid "Build the package for the given architecture." msgstr "" -#: ../pakfire/cli.py:199 ../pakfire/cli.py:221 +#: ../pakfire/cli.py:246 ../pakfire/cli.py:268 msgid "Path were the output files should be copied to." msgstr "" -#: ../pakfire/cli.py:204 +#: ../pakfire/cli.py:251 msgid "Go into a shell." msgstr "" -#: ../pakfire/cli.py:206 ../pakfire/cli.py:217 +#: ../pakfire/cli.py:253 ../pakfire/cli.py:264 msgid "Give name of a package." msgstr "" -#: ../pakfire/cli.py:210 +#: ../pakfire/cli.py:257 msgid "Emulated architecture in the shell." msgstr "" -#: ../pakfire/cli.py:215 +#: ../pakfire/cli.py:262 msgid "Generate a source package." msgstr "" -#: ../pakfire/__init__.py:156 +#: ../pakfire/cli.py:272 +msgid "Repository management commands." +msgstr "" + +#: ../pakfire/cli.py:280 +msgid "Create a new repository index." +msgstr "" + +#: ../pakfire/cli.py:281 +msgid "Path to the packages." +msgstr "" + +#: ../pakfire/cli.py:282 +msgid "Path to input packages." +msgstr "" + +#: ../pakfire/__init__.py:165 msgid "Is this okay?" msgstr "" -#: ../pakfire/packages/base.py:42 +#: ../pakfire/packages/base.py:47 msgid "Name" msgstr "" -#: ../pakfire/packages/base.py:43 ../pakfire/transaction.py:231 +#: ../pakfire/packages/base.py:48 ../pakfire/transaction.py:227 msgid "Arch" msgstr "" -#: ../pakfire/packages/base.py:44 ../pakfire/transaction.py:231 +#: ../pakfire/packages/base.py:49 ../pakfire/transaction.py:227 msgid "Version" msgstr "" -#: ../pakfire/packages/base.py:45 +#: ../pakfire/packages/base.py:50 msgid "Release" msgstr "" -#: ../pakfire/packages/base.py:46 ../pakfire/transaction.py:231 +#: ../pakfire/packages/base.py:51 ../pakfire/transaction.py:227 msgid "Size" msgstr "" -#. (_("Repo"), self.repo), -#: ../pakfire/packages/base.py:48 +#: ../pakfire/packages/base.py:52 +msgid "Repo" +msgstr "" + +#: ../pakfire/packages/base.py:53 msgid "Summary" msgstr "" -#. (_("URL"), self.url), -#: ../pakfire/packages/base.py:50 +#: ../pakfire/packages/base.py:54 +msgid "URL" +msgstr "" + +#: ../pakfire/packages/base.py:55 msgid "License" msgstr "" -#: ../pakfire/packages/base.py:53 +#: ../pakfire/packages/base.py:58 msgid "Description" msgstr "" -#: ../pakfire/packages/packager.py:67 +#: ../pakfire/packages/packager.py:70 msgid "Extracting" msgstr "" -#: ../pakfire/packages/packager.py:122 +#: ../pakfire/packages/packager.py:125 msgid "Extracting:" msgstr "" -#: ../pakfire/transaction.py:231 +#: ../pakfire/transaction.py:227 msgid "Package" msgstr "" -#: ../pakfire/transaction.py:231 +#: ../pakfire/transaction.py:227 msgid "Repository" msgstr "" -#: ../pakfire/transaction.py:235 +#: ../pakfire/transaction.py:231 msgid "Installing:" msgstr "" -#: ../pakfire/transaction.py:241 +#: ../pakfire/transaction.py:237 msgid "Updating:" msgstr "" -#: ../pakfire/transaction.py:247 +#: ../pakfire/transaction.py:243 msgid "Removing:" msgstr "" -#: ../pakfire/transaction.py:252 +#: ../pakfire/transaction.py:248 msgid "Transaction Summary" msgstr "" -#: ../pakfire/transaction.py:258 +#: ../pakfire/transaction.py:254 msgid "Install" msgstr "" -#: ../pakfire/transaction.py:258 ../pakfire/transaction.py:261 -#: ../pakfire/transaction.py:264 +#: ../pakfire/transaction.py:254 ../pakfire/transaction.py:257 +#: ../pakfire/transaction.py:260 msgid "Package(s)" msgstr "" -#: ../pakfire/transaction.py:261 +#: ../pakfire/transaction.py:257 msgid "Updates" msgstr "" -#: ../pakfire/transaction.py:264 +#: ../pakfire/transaction.py:260 msgid "Remove" msgstr "" -#: ../pakfire/transaction.py:267 +#: ../pakfire/transaction.py:263 #, python-format msgid "Total download size: %s" msgstr ""