Merge ~ines-almeida/launchpad:prevent-stray-temp-file-on-exception into launchpad:master

Proposed by Ines Almeida
Status: Merged
Approved by: Ines Almeida
Approved revision: 3c271b6d29db90cb616038bed3f054ce90ce0a16
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~ines-almeida/launchpad:prevent-stray-temp-file-on-exception
Merge into: launchpad:master
Diff against target: 102 lines (+55/-3)
2 files modified
lib/lp/archivepublisher/diskpool.py (+15/-2)
lib/lp/archivepublisher/tests/test_pool.py (+40/-1)
Reviewer Review Type Date Requested Status
Colin Watson (community) Approve
Review via email: mp+440381@code.launchpad.net

Commit message

Cleanup temp files if copying component raises

If something goes wrong while copying a component, we:
 * catch the exception (generally, whichever exception)
 * do some cleanup of temporary files
 * re-raise the same exception

LP: #1662671

Description of the change

Added a couple of unit tests that test:
 - the cleanup method removes the temporary path
 - the cleanup is called when an exception is raised, and the exact same exception is re-raised

To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/archivepublisher/diskpool.py b/lib/lp/archivepublisher/diskpool.py
2index c7b2576..18e4124 100644
3--- a/lib/lp/archivepublisher/diskpool.py
4+++ b/lib/lp/archivepublisher/diskpool.py
5@@ -149,6 +149,11 @@ class _diskpool_atomicfile:
6 # different filesystems.
7 self.tempname.rename(self.targetfilename)
8
9+ def cleanup_temporary_path(self) -> None:
10+ """Removes temporary path created on __init__"""
11+ if self.tempname.exists():
12+ self.tempname.unlink()
13+
14
15 class DiskPoolEntry:
16 """Represents a single file in the pool, across all components.
17@@ -285,8 +290,16 @@ class DiskPoolEntry:
18 file_to_write = _diskpool_atomicfile(
19 targetpath, "wb", rootpath=self.temppath
20 )
21- lfa.open()
22- copy_and_close(lfa, file_to_write)
23+ try:
24+ lfa.open()
25+ copy_and_close(lfa, file_to_write)
26+ except Exception:
27+ # Prevent ending up with a stray temporary file lying around if
28+ # anything goes wrong while copying the file. Still raises error
29+ self.debug("Cleaning up temp path %s" % file_to_write.tempname)
30+ file_to_write.cleanup_temporary_path()
31+ raise
32+
33 self.file_component = component
34 return FileAddActionEnum.FILE_ADDED
35
36diff --git a/lib/lp/archivepublisher/tests/test_pool.py b/lib/lp/archivepublisher/tests/test_pool.py
37index 311f203..1329e34 100644
38--- a/lib/lp/archivepublisher/tests/test_pool.py
39+++ b/lib/lp/archivepublisher/tests/test_pool.py
40@@ -5,11 +5,17 @@
41
42 import hashlib
43 from pathlib import Path, PurePath
44+from unittest import mock
45
46 from lazr.enum import EnumeratedType, Item
47 from zope.interface import alsoProvides, implementer
48
49-from lp.archivepublisher.diskpool import DiskPool, poolify, unpoolify
50+from lp.archivepublisher.diskpool import (
51+ DiskPool,
52+ _diskpool_atomicfile,
53+ poolify,
54+ unpoolify,
55+)
56 from lp.registry.interfaces.sourcepackage import SourcePackageFileType
57 from lp.services.log.logger import BufferLogger
58 from lp.soyuz.enums import ArchiveRepositoryFormat, BinaryPackageFileType
59@@ -102,6 +108,10 @@ class FakePackageReleaseFile:
60 alsoProvides(self, IBinaryPackageFile)
61
62
63+class SpecificTestException(Exception):
64+ pass
65+
66+
67 class PoolTestingFile:
68 def __init__(
69 self,
70@@ -290,3 +300,32 @@ class TestPool(TestCase):
71 foo.removeFromPool("main")
72 self.assertFalse(foo.checkExists("main"))
73 self.assertTrue(foo.checkIsFile("universe"))
74+
75+ def test_raise_deletes_temporary_file(self):
76+ """If copying fails, cleanup is called and same error is raised"""
77+ foo = PoolTestingFile(
78+ pool=self.pool,
79+ source_name="foo",
80+ source_version="1.0",
81+ filename="foo-1.0.deb",
82+ )
83+
84+ with mock.patch(
85+ "lp.archivepublisher.diskpool.copy_and_close",
86+ side_effect=SpecificTestException,
87+ ), mock.patch.object(
88+ _diskpool_atomicfile, "cleanup_temporary_path"
89+ ) as mock_cleanup:
90+ self.assertRaises(SpecificTestException, foo.addToPool, "universe")
91+
92+ self.assertEqual(mock_cleanup.call_count, 1)
93+ self.assertFalse(foo.checkIsFile("universe"))
94+
95+ def test_diskpool_atomicfile(self):
96+ """Temporary files are properly removed"""
97+ target_path = Path(self.makeTemporaryDirectory() + "/temp.file")
98+ foo = _diskpool_atomicfile(target_path, "wb")
99+
100+ self.assertTrue(foo.tempname.exists())
101+ foo.cleanup_temporary_path()
102+ self.assertFalse(foo.tempname.exists())

Subscribers

People subscribed via source and target branches

to status/vote changes: