launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #20100
[Merge] lp:~cjwatson/launchpad/archive-file-model into lp:launchpad
Colin Watson has proposed merging lp:~cjwatson/launchpad/archive-file-model into lp:launchpad.
Commit message:
Add model for ArchiveFile.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #1430011 in Launchpad itself: "support apt by-hash mirrors"
https://bugs.launchpad.net/launchpad/+bug/1430011
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/archive-file-model/+merge/289376
Add model for ArchiveFile. This will be used to implement by-hash, and the current set of methods is tailored to that. It should be possible to use it for diskless archives as well later on.
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/archive-file-model into lp:launchpad.
=== modified file 'lib/lp/soyuz/configure.zcml'
--- lib/lp/soyuz/configure.zcml 2016-03-04 14:18:23 +0000
+++ lib/lp/soyuz/configure.zcml 2016-03-17 14:39:54 +0000
@@ -422,6 +422,23 @@
interface="lp.soyuz.interfaces.archive.IArchiveSet"/>
</securedutility>
+ <!-- ArchiveFile -->
+
+ <class class="lp.soyuz.model.archivefile.ArchiveFile">
+ <allow interface="lp.soyuz.interfaces.archivefile.IArchiveFile"/>
+ </class>
+
+ <!-- ArchiveFileSet -->
+
+ <class class="lp.soyuz.model.archivefile.ArchiveFileSet">
+ <allow interface="lp.soyuz.interfaces.archivefile.IArchiveFileSet"/>
+ </class>
+ <securedutility
+ class="lp.soyuz.model.archivefile.ArchiveFileSet"
+ provides="lp.soyuz.interfaces.archivefile.IArchiveFileSet">
+ <allow interface="lp.soyuz.interfaces.archivefile.IArchiveFileSet"/>
+ </securedutility>
+
<!-- ArchiveJob -->
<class class="lp.soyuz.model.archivejob.ArchiveJob">
=== added file 'lib/lp/soyuz/interfaces/archivefile.py'
--- lib/lp/soyuz/interfaces/archivefile.py 1970-01-01 00:00:00 +0000
+++ lib/lp/soyuz/interfaces/archivefile.py 2016-03-17 14:39:54 +0000
@@ -0,0 +1,112 @@
+# Copyright 2016 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Interface for a file in an archive."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'IArchiveFile',
+ 'IArchiveFileSet',
+ ]
+
+from lazr.restful.fields import Reference
+from zope.interface import Interface
+from zope.schema import (
+ Datetime,
+ Int,
+ TextLine,
+ )
+
+from lp import _
+from lp.services.librarian.interfaces import ILibraryFileAlias
+from lp.soyuz.interfaces.archive import IArchive
+
+
+class IArchiveFile(Interface):
+ """A file in an archive.
+
+ This covers files that are not published in the archive's package pool,
+ such as the Packages and Sources index files.
+ """
+
+ id = Int(title=_("ID"), required=True, readonly=True)
+
+ archive = Reference(
+ title=_("The archive containing the index file."),
+ schema=IArchive, required=True, readonly=True)
+
+ container = TextLine(
+ title=_("An identifier for the component that manages this file."),
+ required=True, readonly=True)
+
+ path = TextLine(
+ title=_("The path to the index file within the published archive."),
+ required=True, readonly=True)
+
+ library_file = Reference(
+ title=_("The index file in the librarian."),
+ schema=ILibraryFileAlias, required=True, readonly=True)
+
+ scheduled_deletion_date = Datetime(
+ title=_("The date when this file should stop being published."),
+ required=False, readonly=False)
+
+
+class IArchiveFileSet(Interface):
+ """Bulk operations on files in an archive."""
+
+ def new(archive, container, path, library_file):
+ """Create a new `IArchiveFile`.
+
+ :param archive: The `IArchive` containing the new file.
+ :param container: An identifier for the component that manages this
+ file.
+ :param path: The path to the new file within its archive.
+ :param library_file: The `ILibraryFileAlias` embodying the new file.
+ """
+
+ def newFromFile(archive, container, root, path, size, content_type):
+ """Create a new `IArchiveFile` from a file on the file system.
+
+ :param archive: The `IArchive` containing the new file.
+ :param container: An identifier for the component that manages this
+ file.
+ :param root: The path to the root of the archive.
+ :param path: The path to the new file within its archive.
+ :param size: The size of the file in bytes.
+ :param content_type: The MIME type of the file.
+ """
+
+ def getByArchive(archive, container=None, eager_load=False):
+ """Get files in an archive.
+
+ :param archive: Return files in this `IArchive`.
+ :param container: Return only files with this container.
+ :param eager_load: If True, preload related `LibraryFileAlias` and
+ `LibraryFileContent` rows.
+ """
+
+ def scheduleDeletion(archive_files, stay_of_execution):
+ """Schedule these archive files for future deletion.
+
+ :param archive_files: The `IArchiveFile`s to schedule for deletion.
+ :param stay_of_execution: A `timedelta`; schedule files for deletion
+ this amount of time in the future.
+ """
+
+ def getContainersToReap(archive, container_prefix=None):
+ """Return containers in this archive with files that should be reaped.
+
+ :param archive: Return containers in this `IArchive`.
+ :param container_prefix: Return only containers that start with this
+ prefix.
+ """
+
+ def reap(archive, container=None):
+ """Delete archive files that are past their scheduled deletion date.
+
+ :param archive: Delete files from this `IArchive`.
+ :param container: Delete only files with this container.
+ """
=== added file 'lib/lp/soyuz/model/archivefile.py'
--- lib/lp/soyuz/model/archivefile.py 1970-01-01 00:00:00 +0000
+++ lib/lp/soyuz/model/archivefile.py 2016-03-17 14:39:54 +0000
@@ -0,0 +1,143 @@
+# Copyright 2016 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""A file in an archive."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'ArchiveFile',
+ 'ArchiveFileSet',
+ ]
+
+import os.path
+
+import pytz
+from storm.locals import (
+ DateTime,
+ Int,
+ Reference,
+ Storm,
+ Unicode,
+ )
+from zope.component import getUtility
+from zope.interface import implementer
+
+from lp.services.database.bulk import load_related
+from lp.services.database.constants import UTC_NOW
+from lp.services.database.decoratedresultset import DecoratedResultSet
+from lp.services.database.interfaces import (
+ IMasterStore,
+ IStore,
+ )
+from lp.services.librarian.interfaces import ILibraryFileAliasSet
+from lp.services.librarian.model import (
+ LibraryFileAlias,
+ LibraryFileContent,
+ )
+from lp.soyuz.interfaces.archivefile import (
+ IArchiveFile,
+ IArchiveFileSet,
+ )
+
+
+@implementer(IArchiveFile)
+class ArchiveFile(Storm):
+ """See `IArchiveFile`."""
+
+ __storm_table__ = 'ArchiveFile'
+
+ id = Int(primary=True)
+
+ archive_id = Int(name='archive', allow_none=False)
+ archive = Reference(archive_id, 'Archive.id')
+
+ container = Unicode(name='container', allow_none=False)
+
+ path = Unicode(name='path', allow_none=False)
+
+ library_file_id = Int(name='library_file', allow_none=False)
+ library_file = Reference(library_file_id, 'LibraryFileAlias.id')
+
+ scheduled_deletion_date = DateTime(
+ name='scheduled_deletion_date', tzinfo=pytz.UTC, allow_none=True)
+
+ def __init__(self, archive, container, path, library_file):
+ """Construct an `ArchiveFile`."""
+ super(ArchiveFile, self).__init__()
+ self.archive = archive
+ self.container = container
+ self.path = path
+ self.library_file = library_file
+ self.scheduled_deletion_date = None
+
+
+@implementer(IArchiveFileSet)
+class ArchiveFileSet:
+ """See `IArchiveFileSet`."""
+
+ @staticmethod
+ def new(archive, container, path, library_file):
+ """See `IArchiveFileSet`."""
+ archive_file = ArchiveFile(archive, container, path, library_file)
+ IMasterStore(ArchiveFile).add(archive_file)
+ return archive_file
+
+ @classmethod
+ def newFromFile(cls, archive, container, root, path, size, content_type):
+ with open(os.path.join(root, path), "rb") as f:
+ library_file = getUtility(ILibraryFileAliasSet).create(
+ os.path.basename(path), size, f, content_type,
+ restricted=archive.private)
+ return cls.new(archive, container, path, library_file)
+
+ @staticmethod
+ def getByArchive(archive, container=None, eager_load=False):
+ """See `IArchiveFileSet`."""
+ clauses = [ArchiveFile.archive == archive]
+ # XXX cjwatson 2016-03-15: We'll need some more sophisticated way to
+ # match containers once we're using them for custom uploads.
+ if container is not None:
+ clauses.append(ArchiveFile.container == container)
+ archive_files = IStore(ArchiveFile).find(ArchiveFile, *clauses)
+
+ def eager_load(rows):
+ lfas = load_related(LibraryFileAlias, rows, ["library_file_id"])
+ load_related(LibraryFileContent, lfas, ["contentID"])
+
+ if eager_load:
+ return DecoratedResultSet(archive_files, pre_iter_hook=eager_load)
+ else:
+ return archive_files
+
+ @staticmethod
+ def scheduleDeletion(archive_files, stay_of_execution):
+ """See `IArchiveFileSet`."""
+ archive_file_ids = set(
+ archive_file.id for archive_file in archive_files)
+ rows = IMasterStore(ArchiveFile).find(
+ ArchiveFile, ArchiveFile.id.is_in(archive_file_ids))
+ rows.set(scheduled_deletion_date=UTC_NOW + stay_of_execution)
+
+ @staticmethod
+ def getContainersToReap(archive, container_prefix=None):
+ clauses = [
+ ArchiveFile.archive == archive,
+ ArchiveFile.scheduled_deletion_date < UTC_NOW,
+ ]
+ if container_prefix is not None:
+ clauses.append(ArchiveFile.container.startswith(container_prefix))
+ return IStore(ArchiveFile).find(
+ ArchiveFile.container, *clauses).group_by(ArchiveFile.container)
+
+ @staticmethod
+ def reap(archive, container=None):
+ """See `IArchiveFileSet`."""
+ clauses = [
+ ArchiveFile.archive == archive,
+ ArchiveFile.scheduled_deletion_date < UTC_NOW,
+ ]
+ if container is not None:
+ clauses.append(ArchiveFile.container == container)
+ IMasterStore(ArchiveFile).find(ArchiveFile, *clauses).remove()
=== added file 'lib/lp/soyuz/tests/test_archivefile.py'
--- lib/lp/soyuz/tests/test_archivefile.py 1970-01-01 00:00:00 +0000
+++ lib/lp/soyuz/tests/test_archivefile.py 2016-03-17 14:39:54 +0000
@@ -0,0 +1,153 @@
+# Copyright 2016 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""ArchiveFile tests."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+
+from datetime import (
+ datetime,
+ timedelta,
+ )
+import os
+
+import pytz
+from testtools.matchers import LessThan
+import transaction
+from zope.component import getUtility
+from zope.security.proxy import removeSecurityProxy
+
+from lp.services.osutils import open_for_writing
+from lp.soyuz.interfaces.archivefile import IArchiveFileSet
+from lp.testing import TestCaseWithFactory
+from lp.testing.layers import LaunchpadZopelessLayer
+
+
+class TestArchiveFile(TestCaseWithFactory):
+
+ layer = LaunchpadZopelessLayer
+
+ def test_new(self):
+ archive = self.factory.makeArchive()
+ library_file = self.factory.makeLibraryFileAlias()
+ archive_file = getUtility(IArchiveFileSet).new(
+ archive, "foo", "dists/foo", library_file)
+ self.assertEqual(archive, archive_file.archive)
+ self.assertEqual("foo", archive_file.container)
+ self.assertEqual("dists/foo", archive_file.path)
+ self.assertEqual(library_file, archive_file.library_file)
+ self.assertIsNone(archive_file.scheduled_deletion_date)
+
+ def test_newFromFile(self):
+ root = self.makeTemporaryDirectory()
+ with open_for_writing(os.path.join(root, "dists/foo"), "w") as f:
+ f.write("abc\n")
+ archive = self.factory.makeArchive()
+ archive_file = getUtility(IArchiveFileSet).newFromFile(
+ archive, "foo", root, "dists/foo", 4, "text/plain")
+ transaction.commit()
+ self.assertEqual(archive, archive_file.archive)
+ self.assertEqual("foo", archive_file.container)
+ self.assertEqual("dists/foo", archive_file.path)
+ archive_file.library_file.open()
+ try:
+ self.assertEqual("abc\n", archive_file.library_file.read())
+ finally:
+ archive_file.library_file.close()
+ self.assertIsNone(archive_file.scheduled_deletion_date)
+
+ def test_getByArchive(self):
+ archives = [self.factory.makeArchive(), self.factory.makeArchive()]
+ archive_files = []
+ for archive in archives:
+ archive_files.append(self.factory.makeArchiveFile(archive=archive))
+ archive_files.append(self.factory.makeArchiveFile(
+ archive=archive, container="foo"))
+ archive_file_set = getUtility(IArchiveFileSet)
+ self.assertContentEqual(
+ archive_files[:2], archive_file_set.getByArchive(archives[0]))
+ self.assertContentEqual(
+ [archive_files[1]],
+ archive_file_set.getByArchive(archives[0], container="foo"))
+ self.assertContentEqual(
+ [], archive_file_set.getByArchive(archives[0], container="bar"))
+ self.assertContentEqual(
+ archive_files[2:], archive_file_set.getByArchive(archives[1]))
+ self.assertContentEqual(
+ [archive_files[3]],
+ archive_file_set.getByArchive(archives[1], container="foo"))
+ self.assertContentEqual(
+ [], archive_file_set.getByArchive(archives[1], container="bar"))
+
+ def test_scheduleDeletion(self):
+ archive_files = [self.factory.makeArchiveFile() for _ in range(3)]
+ getUtility(IArchiveFileSet).scheduleDeletion(
+ archive_files[:2], timedelta(days=1))
+ tomorrow = datetime.now(pytz.UTC) + timedelta(days=1)
+ # Allow a bit of timing slack for slow tests.
+ self.assertThat(
+ tomorrow - archive_files[0].scheduled_deletion_date,
+ LessThan(timedelta(minutes=5)))
+ self.assertThat(
+ tomorrow - archive_files[1].scheduled_deletion_date,
+ LessThan(timedelta(minutes=5)))
+ self.assertIsNone(archive_files[2].scheduled_deletion_date)
+
+ def test_getContainersToReap(self):
+ archive = self.factory.makeArchive()
+ archive_files = []
+ for container in ("release:foo", "other:bar", "baz"):
+ for _ in range(2):
+ archive_files.append(self.factory.makeArchiveFile(
+ archive=archive, container=container))
+ other_archive = self.factory.makeArchive()
+ archive_files.append(self.factory.makeArchiveFile(
+ archive=other_archive, container="baz"))
+ now = datetime.now(pytz.UTC)
+ removeSecurityProxy(archive_files[0]).scheduled_deletion_date = (
+ now - timedelta(days=1))
+ removeSecurityProxy(archive_files[1]).scheduled_deletion_date = (
+ now - timedelta(days=1))
+ removeSecurityProxy(archive_files[2]).scheduled_deletion_date = (
+ now + timedelta(days=1))
+ removeSecurityProxy(archive_files[6]).scheduled_deletion_date = (
+ now - timedelta(days=1))
+ archive_file_set = getUtility(IArchiveFileSet)
+ self.assertContentEqual(
+ ["release:foo"], archive_file_set.getContainersToReap(archive))
+ self.assertContentEqual(
+ ["baz"], archive_file_set.getContainersToReap(other_archive))
+ removeSecurityProxy(archive_files[3]).scheduled_deletion_date = (
+ now - timedelta(days=1))
+ self.assertContentEqual(
+ ["release:foo", "other:bar"],
+ archive_file_set.getContainersToReap(archive))
+ self.assertContentEqual(
+ ["release:foo"],
+ archive_file_set.getContainersToReap(
+ archive, container_prefix="release:"))
+
+ def test_reap(self):
+ archive = self.factory.makeArchive()
+ archive_files = [
+ self.factory.makeArchiveFile(archive=archive, container="foo")
+ for _ in range(3)]
+ archive_files.append(self.factory.makeArchiveFile(archive=archive))
+ other_archive = self.factory.makeArchive()
+ archive_files.append(
+ self.factory.makeArchiveFile(archive=other_archive))
+ now = datetime.now(pytz.UTC)
+ removeSecurityProxy(archive_files[0]).scheduled_deletion_date = (
+ now - timedelta(days=1))
+ removeSecurityProxy(archive_files[1]).scheduled_deletion_date = (
+ now + timedelta(days=1))
+ removeSecurityProxy(archive_files[3]).scheduled_deletion_date = (
+ now - timedelta(days=1))
+ removeSecurityProxy(archive_files[4]).scheduled_deletion_date = (
+ now - timedelta(days=1))
+ archive_file_set = getUtility(IArchiveFileSet)
+ archive_file_set.reap(archive, container="foo")
+ self.assertContentEqual(
+ archive_files[1:4], archive_file_set.getByArchive(archive))
=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py 2016-03-09 01:37:56 +0000
+++ lib/lp/testing/factory.py 2016-03-17 14:39:54 +0000
@@ -289,6 +289,7 @@
default_name_by_purpose,
IArchiveSet,
)
+from lp.soyuz.interfaces.archivefile import IArchiveFileSet
from lp.soyuz.interfaces.archivepermission import IArchivePermissionSet
from lp.soyuz.interfaces.binarypackagebuild import IBinaryPackageBuildSet
from lp.soyuz.interfaces.binarypackagename import IBinaryPackageNameSet
@@ -2878,6 +2879,20 @@
permission_set.newQueueAdmin(archive, person, 'main')
return person
+ def makeArchiveFile(self, archive=None, container=None, path=None,
+ library_file=None):
+ if archive is None:
+ archive = self.makeArchive()
+ if container is None:
+ container = self.getUniqueUnicode()
+ if path is None:
+ path = self.getUniqueUnicode()
+ if library_file is None:
+ library_file = self.makeLibraryFileAlias()
+ return getUtility(IArchiveFileSet).new(
+ archive=archive, container=container, path=path,
+ library_file=library_file)
+
def makeBuilder(self, processors=None, url=None, name=None, title=None,
owner=None, active=True, virtualized=True, vm_host=None,
vm_reset_protocol=None, manual=False):
Follow ups