Skip to content

Commit

Permalink
add restore methods for storage and records
Browse files Browse the repository at this point in the history
Add restore methods that are able to restore from their respective dump
methods.
  • Loading branch information
enku committed Feb 16, 2025
1 parent 8c05992 commit b9c8af3
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 1 deletion.
41 changes: 40 additions & 1 deletion src/gentoo_build_publisher/records/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
DumpCallback,
default_dump_callback,
)
from gentoo_build_publisher.utils import serializable
from gentoo_build_publisher.utils import convert_to, decode_to, serializable


class RecordNotFound(LookupError):
Expand Down Expand Up @@ -120,6 +120,16 @@ def dump(
See also dump_build_records below which is a function that already does this.
"""

def restore(
self, infile: IO[bytes], *, callback: DumpCallback = default_dump_callback
) -> list[BuildRecord]:
"""Restore to the db the records given in the infile
The infile should be structured with the dump() method.
See also restore_build_records below which is a function that already does this.
"""


def build_records(settings: Settings) -> RecordDB:
"""Return instance of the the RecordDB class given in settings"""
Expand Down Expand Up @@ -199,3 +209,32 @@ def dump_build_records(

serialized = json.dumps(build_list, default=serializable)
outfile.write(serialized.encode("utf8"))


@convert_to(BuildRecord, "built")
@convert_to(BuildRecord, "completed")
@convert_to(BuildRecord, "submitted")
def _(value: str | None) -> dt.datetime | None:
return None if value is None else dt.datetime.fromisoformat(value)


def restore_build_records(
infile: IO[bytes],
records: RecordDB,
*,
callback: DumpCallback = default_dump_callback,
) -> list[BuildRecord]:
"""Restore the JSON given in the infile to BuildRecords in the given RecordDB
Return the restored records
"""
restore_list: list[BuildRecord] = []

items = json.load(infile)
for item in items:
record = decode_to(BuildRecord, item)
callback("restore", "records", record)
record = records.save(record)
restore_list.append(record)

return restore_list
10 changes: 10 additions & 0 deletions src/gentoo_build_publisher/records/django_orm.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
BuildRecord,
RecordNotFound,
dump_build_records,
restore_build_records,
)
from gentoo_build_publisher.types import (
ApiKey,
Expand Down Expand Up @@ -224,6 +225,15 @@ def dump(
"""
dump_build_records(builds, outfile, callback=callback)

def restore(
self, infile: IO[bytes], *, callback: DumpCallback = default_dump_callback
) -> list[BuildRecord]:
"""Restore to the db the records given in the infile
The infile should be structured with the dump() method.
"""
return restore_build_records(infile, self, callback=callback)


class ApiKeyDB:
"""Implements the ApiKeyDB Protocol using Django's ORM as a backing store"""
Expand Down
10 changes: 10 additions & 0 deletions src/gentoo_build_publisher/records/memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
BuildRecord,
RecordNotFound,
dump_build_records,
restore_build_records,
)
from gentoo_build_publisher.types import (
ApiKey,
Expand Down Expand Up @@ -198,6 +199,15 @@ def dump(
"""
dump_build_records(builds, outfile, callback=callback)

def restore(
self, infile: t.IO[bytes], *, callback: DumpCallback = default_dump_callback
) -> list[BuildRecord]:
"""Restore to the db the records given in the infile
The infile should be structured with the dump() method.
"""
return restore_build_records(infile, self, callback=callback)


def record_key(record: BuildRecord) -> int | str:
"""Sort key function for records (of the same machine)"""
Expand Down
30 changes: 30 additions & 0 deletions src/gentoo_build_publisher/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,26 @@ def dump(
path = path.relative_to(self.root)
tarfile.add(path)

def restore(
self, fp: IO[bytes], *, callback: DumpCallback = default_dump_callback
) -> list[Build]:
"""Restore builds from the given file object
This is the complement of dump()
Return the list of builds restored.
"""
restore_list: list[Build] = []

with tar.open(fileobj=fp, mode="r|") as tarfile, fs.cd(self.root):
for member in tarfile:
if is_content_dir(member, Content.REPOS):
build = Build.from_id(member.name.split("/", 1)[1])
restore_list.append(build)
callback("restore", "storage", build)
tarfile.extract(member)

return restore_list

def get_metadata(self, build: Build) -> GBPMetadata:
"""Read binpkg/gbp.json and return GBPMetadata instance
Expand Down Expand Up @@ -375,3 +395,13 @@ def make_packages(package_index_file: IO[str]) -> Iterable[Package]:
"""
for section in string.get_sections(package_index_file):
yield make_package_from_lines(section)


def is_content_dir(member: tar.TarInfo, content_type: Content) -> bool:
"""Return true if the given TarFile member is a repo directory"""
if not member.isdir():
return False

parts = member.name.split("/")

return len(parts) == 2 and parts[0] == content_type.value
20 changes: 20 additions & 0 deletions tests/test_records.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,26 @@ def test_dump(self, backend: str) -> None:

self.assertEqual([build], [BuildRecord(**item) for item in dump])

@parametrized(BACKENDS)
def test_restore(self, backend: str) -> None:
records = self.backend(backend)
record = BuildRecordFactory.create()
record = records.save(record)
path = self.fixtures.tmpdir / "records.json"

with open(path, "wb") as outfile:
records.dump([record], outfile)

records.delete(record)

with open(path, "rb") as infile:
response = records.restore(infile)

self.assertEqual([record], response)
self.assertEqual(
record, records.get(Build(machine=record.machine, build_id=record.build_id))
)


class BuildRecordsTestCase(TestCase):
def test_django(self) -> None:
Expand Down
30 changes: 30 additions & 0 deletions tests/test_storage.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Tests for the storage type"""

# pylint: disable=missing-class-docstring,missing-function-docstring
import io
import json
import os
import tarfile
Expand Down Expand Up @@ -694,3 +695,32 @@ def test(self) -> None:
self.assertIn(f"var-lib-portage/{bid}", contents)
self.assertIn(f"var-lib-portage/{build.machine}", contents)
self.assertIn(f"var-lib-portage/{build.machine}@mytag", contents)


@fixture.requires("tmpdir", "publisher", "build")
class StorageRestoreTests(TestCase):
"""Tests for storage.restore"""

def test(self) -> None:
# Given the pulled build
build = self.fixtures.build
publisher.pull(build)
publisher.publish(build)
publisher.tag(build, "mytag")

# Given the dump of it
fp = io.BytesIO()
storage = publisher.storage
storage.dump([build], fp)

# When we run restore on it
storage.delete(build)
self.assertFalse(storage.pulled(build))
fp.seek(0)
restored = storage.restore(fp)

# Then we get the builds restored
self.assertEqual([build], restored)
self.assertTrue(storage.pulled(build))
tags = storage.get_tags(build)
self.assertEqual(["", "mytag"], tags)

0 comments on commit b9c8af3

Please sign in to comment.