From: Michael Tremer Date: Thu, 14 Jul 2022 14:35:36 +0000 (+0000) Subject: packages: Make files downloadable again X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=47e3052a58dfd8e8a8080fae945bbbf73125c5b7;p=pbs.git packages: Make files downloadable again Signed-off-by: Michael Tremer --- diff --git a/src/buildservice/packages.py b/src/buildservice/packages.py index 632cfa78..cfd0fd4c 100644 --- a/src/buildservice/packages.py +++ b/src/buildservice/packages.py @@ -1,7 +1,9 @@ #!/usr/bin/python +import asyncio import datetime import logging +import mimetypes import os import shutil import urllib.parse @@ -417,7 +419,7 @@ class Package(base.DataObject): ret = [] for row in res: - f = File(self.backend, row) + f = File(self.backend, self, row) ret.append(f) return ret @@ -427,7 +429,7 @@ class Package(base.DataObject): WHERE pkg_id = %s AND name = %s", self.id, filename) if res: - return File(self.backend, res) + return File(self.backend, self, res) def add_file(self, name, size, hash_sha512, type, config, mode, user, group, mtime, capabilities): # Convert mtime from seconds since epoch to datetime @@ -437,11 +439,11 @@ class Package(base.DataObject): \"user\", \"group\", mtime, capabilities) VALUES(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)", self.id, name, size, hash_sha512, type, config, mode, user, group, mtime, capabilities) - def open(self): - path = os.path.join(PACKAGES_DIR, self.path) - - if os.path.exists(path): - return pakfire.packages.open(None, None, path) + async def open(self): + """ + Opens the package archive and returns a handle + """ + return await self.backend.open(self.path) ## properties @@ -476,22 +478,58 @@ class Package(base.DataObject): class File(base.Object): - def init(self, data): + def init(self, package, data): + self.package = package self.data = data - def __getattr__(self, attr): - try: - return self.data[attr] - except KeyError: - raise AttributeError(attr) + @property + def name(self): + return self.data.name @property - def downloadable(self): - # All regular files are downloadable. - return self.type == 0 + def size(self): + return self.data.size + + @property + def hash_sha512(self): + return self.data.hash_sha512 + + @property + def type(self): + return self.data.type + + @property + def config(self): + return self.data.config + + @property + def mode(self): + return self.data.mode + + @property + def user(self): + return self.data.user @property - def viewable(self): + def group(self): + return self.data.group + + @property + def mtime(self): + return self.data.mtime + + @property + def capabilities(self): + return self.data.capabilities + + def is_downloadable(self): + """ + Returns True if this file is downloadable + """ + # All regular files are downloadable + return self.type == 0 + + def is_viewable(self): # Empty files cannot be viewed. if self.size == 0: return False @@ -501,3 +539,28 @@ class File(base.Object): return True return False + + @property + def mimetype(self): + """ + The (guessed) MIME type of this file + """ + # Guess the MIME type of the file. + type, encoding = mimetypes.guess_type(self.name) + + return type or "application/octet-stream" + + # Payload + + async def get_payload(self): + """ + Fetches and returns the payload + """ + # Open the package + p = await self.package.open() + + # Create a helper function to read the entire payload + func = lambda: p.read(self.name) + + # Read the payload in a separate thread + return await asyncio.to_thread(func) diff --git a/src/templates/modules/packages-files-table.html b/src/templates/modules/packages-files-table.html index 5d25ed67..73a8d42f 100644 --- a/src/templates/modules/packages-files-table.html +++ b/src/templates/modules/packages-files-table.html @@ -20,14 +20,14 @@ {{ file.name }} - {% if file.viewable %} + {% if file.is_viewable() %} {{ _("View file") }} {% end %} - {% if file.downloadable %} + {% if file.is_downloadable() %} {{ _("Download") }} diff --git a/src/web/__init__.py b/src/web/__init__.py index 3f928a72..e02c8b8d 100644 --- a/src/web/__init__.py +++ b/src/web/__init__.py @@ -134,7 +134,8 @@ class Application(tornado.web.Application): (r"/packages", packages.IndexHandler), (r"/packages/([\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12})", packages.ShowHandler), (r"/packages/([\w\-\+]+)", packages.NameHandler), - (r"/package/([\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12})/download(.*)", packages.PackageFileDownloadHandler), + (r"/package/([\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12})/download(.*)", + packages.FileDownloadHandler), (r"/package/([\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12})/view(.*)", packages.PackageFileViewHandler), (r"/package/([\w\-\+]+)/properties", packages.PackagePropertiesHandler), diff --git a/src/web/packages.py b/src/web/packages.py index f1fdd561..1dd88aae 100644 --- a/src/web/packages.py +++ b/src/web/packages.py @@ -1,6 +1,5 @@ #!/usr/bin/python -import mimetypes import os.path import tornado.web @@ -42,7 +41,7 @@ class ShowHandler(base.BaseHandler): def get(self, uuid): package = self.backend.packages.get_by_uuid(uuid) if not package: - return tornado.web.HTTPError(404, "Could not find package: %s" % uuid) + raise tornado.web.HTTPError(404, "Could not find package: %s" % uuid) self.render("packages/show.html", package=package) @@ -83,60 +82,45 @@ class PackagePropertiesHandler(base.BaseHandler): build.pkg.update_property("critical_path", critical_path) -class PackageFileDownloadHandler(base.BaseHandler): - def get_file(self, pkg_uuid, filename): - # Fetch package. - pkg = self.backend.packages.get_by_uuid(pkg_uuid) - if not pkg: - raise tornado.web.HTTPError(404, "Package not found: %s" % pkg_uuid) +class FileDownloadHandler(base.BaseHandler): + async def get(self, uuid, path): + package = self.backend.packages.get_by_uuid(uuid) + if not package: + raise tornado.web.HTTPError(404, "Could not find package: %s" % uuid) - # Check if the package has got a file with the given name. - file = pkg.get_file(filename) + # Fetch the file + file = package.get_file(path) if not file: - raise tornado.web.HTTPError(404, "Package %s does not contain file %s" % (pkg, filename)) - - # Open the package in the filesystem. - pkg_file = pkg.open() - if not pkg_file: - raise tornado.web.HTTPError(404, "Could not open package %s" % pkg.path) - - # Open the file to transfer it to the client. - f = pkg_file.open_file(filename) - if not f: - raise tornado.web.HTTPError(404, "Package %s does not contain file %s" % (pkg_file, filename)) + raise tornado.web.HTTPError(404, "Could not find file %s in %s" % (path, package)) - # Guess the MIME type of the file. - (type, encoding) = mimetypes.guess_type(filename) - if not type: - type = "text/plain" + # Is this file downloadable? + if not file.is_downloadable(): + raise tornado.web.HTTPError(400, "%s cannot be downloaded" % file) - return (pkg, f, type) + # Send Content-Length + self.set_header("Content-Length", file.size) - def get(self, pkg_uuid, filename): - pkg, f, mimetype = self.get_file(pkg_uuid, filename) + # Send MIME type + self.set_header("Content-Type", file.mimetype) - # Send the filename and mimetype in header. - self.set_header("Content-Disposition", "attachment; filename=%s" % os.path.basename(filename)) - self.set_header("Content-Type", mimetype) + # Send the filename + self.set_header("Content-Disposition", + "attachment; filename=%s" % os.path.basename(file.name)) # These pages should not be indexed self.add_header("X-Robots-Tag", "noindex") - # Transfer the content chunk by chunk. - while True: - buf = f.read(BUFFER_SIZE) - if not buf: - break + # XXX this should probably not be done in one large operation. + # Instead we should send the file chunk by chunk. - self.write(buf) - - f.close() + # Fetch the payload + payload = await file.get_payload() - # Done. - self.finish() + # Send the payload to the client + self.finish(payload) -class PackageFileViewHandler(PackageFileDownloadHandler): +class PackageFileViewHandler(FileDownloadHandler): def get(self, pkg_uuid, filename): pkg, f, mimetype = self.get_file(pkg_uuid, filename)