Merge lp:~jml/pkgme-service/directory-already-exists into lp:pkgme-service

Proposed by Jonathan Lange
Status: Merged
Approved by: Jonathan Lange
Approved revision: 73
Merged at revision: 64
Proposed branch: lp:~jml/pkgme-service/directory-already-exists
Merge into: lp:pkgme-service
Diff against target: 226 lines (+112/-34)
2 files modified
src/djpkgme/tasks.py (+95/-24)
src/djpkgme/tests/test_tasks.py (+17/-10)
To merge this branch: bzr merge lp:~jml/pkgme-service/directory-already-exists
Reviewer Review Type Date Requested Status
James Westby (community) Approve
Review via email: mp+94778@code.launchpad.net

Commit message

Handle empty package_names

Description of the change

When submitting a PDF to pkgme-service on production, we get the following error:

Traceback (most recent call last):
  File "/usr/lib/pymodules/python2.6/celery/execute/trace.py", line 47, in trace
    return cls(states.SUCCESS, retval=fun(*args, **kwargs))
  File "/usr/lib/pymodules/python2.6/celery/app/task/__init__.py", line 247, in __call__
    return self.run(*args, **kwargs)
  File "/srv/pkgme-service.canonical.com/production/pkgme-service/sourcecode/../src/djpkgme/tasks.py", line 246, in run
    packaged_app_path = self.build_package(metadata, overrides)
  File "/srv/pkgme-service.canonical.com/production/pkgme-service/sourcecode/../src/djpkgme/tasks.py", line 223, in build_package
    download_path, output_dir, metadata['package_name'])
  File "/srv/pkgme-service.canonical.com/production/pkgme-service/sourcecode/../src/djpkgme/tasks.py", line 156, in prepare_file
    os.makedirs(extraction_path)
  File "/usr/lib/python2.6/os.py", line 157, in makedirs
    mkdir(name, mode)
OSError: [Errno 17] File exists

The error is caused by 'package_name' being an empty string. I've attacked this problem from two sides:
 1. Rely on submitted data from MyApps as little as possible. Especially, do not rely on submitted package_name.
 2. Structure our directory & file creation to avoid name clashes where possible.

The old 'extraction_path' metaphor is gone, replaced with the idea of 'working_path'. i.e. the place where pkgme does its work. It's created in a hard-coded location (WORKING_DIRECTORY_NAME) and then renamed after the package_name once we've figured out what that is.

To avoid clashes, the downloaded file is stored in its own directory, DOWNLOAD_DIRECTORY_NAME. This directory is deleted after the source package is built, in order to guarantee that it doesn't appear in our output tarball. This fixes a previously unknown bug, I think.

In addition, I've documented what's going on with the directories, and what keys this part of the chain actually expects.

To post a comment you must log in.
Revision history for this message
James Westby (james-w) wrote :

Hi,

This looks good, and is much clearer.

99 + # | \-- devportal-metadata.json (moved out before packaging built)

I "packaging built" means the source package being put in to a tarball? It
may be confused with running pkgme and building the source package, so maybe
"moved out of the way before output tarball is created"?

Thanks,

James

review: Approve
Revision history for this message
Jonathan Lange (jml) wrote :

On Mon, Feb 27, 2012 at 3:00 PM, James Westby <email address hidden> wrote:
> Review: Approve
>
> Hi,
>
> This looks good, and is much clearer.
>
> 99      + # | \-- devportal-metadata.json (moved out before packaging built)
>
> I "packaging built" means the source package being put in to a tarball? It
> may be confused with running pkgme and building the source package, so maybe
> "moved out of the way before output tarball is created"?
>

Actually it means 'after pkgme has made the debian/ dir but before it
actually builds the source package'. Will tweak and land.

jml

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/djpkgme/tasks.py'
2--- src/djpkgme/tasks.py 2012-02-25 02:27:54 +0000
3+++ src/djpkgme/tasks.py 2012-02-27 15:19:28 +0000
4@@ -99,7 +99,7 @@
5 """
6 return {
7 MetadataBackend.CATEGORIES: metadata.get('categories', ''),
8- MetadataBackend.PACKAGE_NAME: metadata['package_name'],
9+ MetadataBackend.PACKAGE_NAME: metadata.get('package_name', ''),
10 MetadataBackend.TAGLINE: metadata.get('tagline', ''),
11 }
12
13@@ -149,31 +149,53 @@
14 PkgmeError,
15 )
16
17-
18- def prepare_file(self, path, output_dir, package_name):
19- extraction_path = os.path.join(output_dir, package_name)
20+ # The name of the directory that we store the downloaded file in.
21+ DOWNLOAD_DIRECTORY_NAME = 'download'
22+
23+ # The name of the directory that we create inside of the temporary build
24+ # directory that pkgme does all of its work on.
25+ WORKING_DIRECTORY_NAME = 'working'
26+
27+ def prepare_file(self, path, output_dir):
28+ """Get 'path' ready for automated packaging inside 'output_dir'.
29+
30+ If 'path' is a tarball, then preparing it means extracting it into a
31+ new directory inside 'output_dir'.
32+
33+ If 'path' is a single file, like a PDF, then preparing it means
34+ copying it into a new directory inside 'output_dir'.
35+
36+ Either way, the key thing is that there is a new directory inside
37+ 'output_dir', inside which is the actual contents we want to try to
38+ automatically package.
39+
40+ :param path: The path where the uploaded file lives. Will be a
41+ tarball, PDF or something like that.
42+ :param output_dir: The directory in which to put the prepared version.
43+ :return: A path to a newly-created directory.
44+ """
45+ working_path = os.path.join(output_dir, self.WORKING_DIRECTORY_NAME)
46 try:
47 t = tarfile.open(path)
48 except tarfile.ReadError:
49- os.makedirs(extraction_path)
50- shutil.copy(path, extraction_path)
51+ os.makedirs(working_path)
52+ shutil.copy(path, working_path)
53 else:
54 try:
55- t.extractall(extraction_path)
56+ t.extractall(working_path)
57 finally:
58 t.close()
59 os.unlink(path)
60- return extraction_path
61+ return working_path
62
63- def write_metadata(self, metadata, extraction_path):
64+ def write_metadata(self, metadata, working_path):
65 """Write metadata in a way that the binary backend expects.
66
67 :param metadata: Metadata from the client as a dictionary.
68- :param extraction_path: The directory to store the metadata JSON file
69- in.
70+ :param working_path: The directory to store the metadata JSON file in.
71 """
72 metadata_filename = os.path.join(
73- extraction_path, MetadataBackend.METADATA_FILE)
74+ working_path, MetadataBackend.METADATA_FILE)
75 with open(metadata_filename, 'w') as metadata_file:
76 json.dump(as_pkgme_dict(metadata), metadata_file)
77 return metadata_filename
78@@ -214,33 +236,82 @@
79 return destination
80
81 def build_package(self, metadata, overrides):
82- """Build packaging for 'metadata' and store it all in 'output_tar_path'.
83+ """Build packaging for 'metadata'.
84
85- :return: The URL that the build source package can be downloaded from.
86+ :return: A path on disk to a tarball containing the built source
87+ package.
88 """
89+ # This does a lot of moving files and directories around. A brief
90+ # summary might help:
91+ #
92+ # $output_dir (a temporary directory created by this method)
93+ # |
94+ # +-+ 'download' (removed after packaging is built)
95+ # | \-- <file downloaded from client>
96+ # |
97+ # +-+ 'working' (removed after packaging is built)
98+ # | \-- <extracted contents of downloaded file>
99+ # | \-- devportal-metadata.json (moved out after debian/ directory
100+ # | created but before source package
101+ # | is built)
102+ # +-- $package_name_0.dsc
103+ # +-- $package_name_0.tar.gz
104+ # +-- $package_name_0_source.build
105+ # +-- $package_name_0_source.changes
106+ # +-- devportal-metadata.json (moved here source package is built)
107+ #
108+ # The contents of $output_dir (i.e. the source package files and
109+ # devportal-metadata.json) are tarred up and stored in a preconfigured
110+ # location: PKGME_OUTPUT_DIRECTORY.
111 with TempDir() as temp_dir:
112 output_dir = temp_dir.path
113- download_path = download_file(metadata['package_url'], output_dir)
114- extraction_path = self.prepare_file(
115- download_path, output_dir, metadata['package_name'])
116- metadata_filename = self.write_metadata(metadata, extraction_path)
117+ download_dir = os.path.join(
118+ output_dir, self.DOWNLOAD_DIRECTORY_NAME)
119+ os.mkdir(download_dir)
120+ download_path = download_file(
121+ metadata['package_url'], download_dir)
122+ working_path = self.prepare_file(download_path, output_dir)
123+ metadata_filename = self.write_metadata(metadata, working_path)
124 # Run pkgme: generate the packaging and build a source package.
125 write_packaging(
126- extraction_path, allowed_backend_names=ALLOWED_BACKEND_NAMES)
127+ working_path, allowed_backend_names=ALLOWED_BACKEND_NAMES)
128+ # XXX: This duplicates work done by write_packaging. However, we
129+ # need the package_name in order to create the tarball in a way
130+ # that looks good.
131+ package_name = MetadataBackend(
132+ working_path).get_package_name(metadata)
133 # Move the metadata file out of the way, so it doesn't appear in
134 # the source package.
135 os.rename(
136 metadata_filename,
137 os.path.join(output_dir, MetadataBackend.METADATA_FILE))
138- build_source_package(extraction_path, False)
139+ # Make sure the directory is named after the package, rather than
140+ # a hard-coded string. This means that when the tarball is
141+ # extracted it creates a meaningful directory.
142+ new_working_path = os.path.join(output_dir, package_name)
143+ os.rename(working_path, new_working_path)
144+ build_source_package(new_working_path, False)
145 # Create a new tarball containing the source package and the
146- # metadata file.
147- shutil.rmtree(extraction_path)
148+ # metadata file, but not the pkgme working set.
149+ shutil.rmtree(download_dir)
150+ shutil.rmtree(new_working_path)
151 return self.save_package(
152- output_dir, settings.PKGME_OUTPUT_DIRECTORY,
153- metadata['package_name'])
154+ output_dir, settings.PKGME_OUTPUT_DIRECTORY, package_name)
155
156 def run(self, metadata, overrides):
157+ """Build a source package.
158+
159+ Requires 'metadata' to have:
160+ * callback_url
161+ * package_url
162+
163+ Uses these if they exist:
164+ * errback_url
165+ * myapps_id
166+
167+ Assumes that the backend can determine a non-empty value for
168+ 'package_name'.
169+ """
170 self.get_logger().info(
171 "Building package for %s" % (
172 metadata.get('myapps_id', '<unknown myapps id>'),))
173
174=== modified file 'src/djpkgme/tests/test_tasks.py'
175--- src/djpkgme/tests/test_tasks.py 2012-02-23 12:12:33 +0000
176+++ src/djpkgme/tests/test_tasks.py 2012-02-27 15:19:28 +0000
177@@ -193,9 +193,14 @@
178 metadata = self.factory.make_metadata()
179 package_name = metadata['package_name']
180 task = BuildPackageTask()
181- self.patch(
182- tasks, 'download_file',
183- lambda url, output_dir: self.factory.make_tarball())
184+ def download_file(url, output_dir):
185+ # Create a tarball and stick it in 'output_dir'.
186+ tarball_location = self.factory.make_tarball()
187+ proper_location = os.path.join(
188+ output_dir, os.path.basename(tarball_location))
189+ os.rename(tarball_location, proper_location)
190+ return proper_location
191+ self.patch(tasks, 'download_file', download_file)
192 pkg_db = self.useFixture(DatabaseFixture())
193 self.factory.add_dependencies_for_test_data(pkg_db)
194 temp_dir = self.useFixture(TempDir()).path
195@@ -243,10 +248,11 @@
196 path = self.factory.make_tarball()
197 tarball = tarfile.open(path)
198 top_level_dir = tarball.getnames()[0]
199- package_name = 'apackage'
200 tempdir = self.useFixture(TempDir()).path
201- new_path = task.prepare_file(path, tempdir, package_name)
202- self.assertEqual(os.path.join(tempdir, package_name), new_path)
203+ new_path = task.prepare_file(path, tempdir)
204+ self.assertEqual(
205+ os.path.join(tempdir, BuildPackageTask.WORKING_DIRECTORY_NAME),
206+ new_path)
207 self.assertThat(new_path, DirExists())
208 self.assertThat(os.path.join(new_path, top_level_dir), DirExists())
209
210@@ -258,11 +264,12 @@
211 content = 'some content\n'
212 with open(os.path.join(source_dir, filename), 'w') as f:
213 f.write(content)
214- package_name = 'apackage'
215 tempdir = self.useFixture(TempDir()).path
216- new_path = task.prepare_file(os.path.join(source_dir, filename),
217- tempdir, package_name)
218- self.assertEqual(os.path.join(tempdir, package_name), new_path)
219+ new_path = task.prepare_file(
220+ os.path.join(source_dir, filename), tempdir)
221+ self.assertEqual(
222+ os.path.join(tempdir, BuildPackageTask.WORKING_DIRECTORY_NAME),
223+ new_path)
224 self.assertThat(new_path, DirExists())
225 self.assertThat(os.path.join(new_path, filename),
226 FileContains(content))

Subscribers

People subscribed via source and target branches