← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~henninge/launchpad/devel-bug-338234-leading-slashes into lp:launchpad

 

Henning Eggers has proposed merging lp:~henninge/launchpad/devel-bug-338234-leading-slashes into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  #338234 Strip leading slashes in tarball paths
  https://bugs.launchpad.net/bugs/338234


= Summary =

This is a simple fix to add "lstrip('/')" in the right place to remove
leading slashes from file paths. The branch also contains a new test case
for the TranslationImportQueue class because it has only being tested in doc
tests so far.

In preparation I moved the useful LaunchpadWriteTarFile helper class in a
module under lp.services because it is not translation specific

== Proposed fix ==

The method addOrUpdateEntriesFromTarball calls _makePath to modify and
filter the path. This is the right place to strip any leading slashes.

== Pre-implementation notes ==

I chatted with Danilo about the location and name for tarfile_helpers.py.

== Tests ==

bin/test -vv \
    -t translationimportqueue \
    -t tarfile_helpers

== Demo and Q/A ==

For QA upload a tarfile which contains filenames with leading slashes and
verify that the queue entries do not have the leadind slashes.

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/translations/utilities/translation_export.py
  lib/lp/translations/model/translationimportqueue.py
  lib/lp/translations/scripts/language_pack.py
  lib/lp/translations/doc/translationimportqueue.txt
  lib/lp/services/tarfile_helpers.py
  lib/lp/services/doc/tarfile_helpers.txt
  lib/lp/translations/tests/test_translationimportqueue.py
-- 
https://code.launchpad.net/~henninge/launchpad/devel-bug-338234-leading-slashes/+merge/44054
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~henninge/launchpad/devel-bug-338234-leading-slashes into lp:launchpad.
=== renamed file 'lib/lp/translations/utilities/doc/launchpad_write_tarfile.txt' => 'lib/lp/services/doc/tarfile_helpers.txt'
--- lib/lp/translations/utilities/doc/launchpad_write_tarfile.txt	2010-04-26 16:00:31 +0000
+++ lib/lp/services/doc/tarfile_helpers.txt	2010-12-17 14:19:59 +0000
@@ -1,11 +1,11 @@
-= Launchpad helper to write tar files =
+Launchpad helper to write tar files
+===================================
 
 LaunchpadWriteTarFile is a helper class to make .tar.gz generation
 easy.
 
     >>> from StringIO import StringIO
-    >>> from lp.translations.utilities.translation_export import (
-    ...     LaunchpadWriteTarFile)
+    >>> from lp.services.tarfile_helpers import LaunchpadWriteTarFile
 
 First, we are going to define a function we are going to use to validate the
 output we will get.
@@ -99,9 +99,9 @@
     ...     'uno/dos/tres/cuatro': 'blah'
     ...     })
     >>> examine_tarfile(archive)
-    uno                 | 
-    uno/dos             | 
-    uno/dos/tres        | 
+    uno                 |
+    uno/dos             |
+    uno/dos/tres        |
     uno/dos/tres/cuatro | blah
 
 Also, if there is a duplicated file, last one is the one that remains there.

=== added file 'lib/lp/services/tarfile_helpers.py'
--- lib/lp/services/tarfile_helpers.py	1970-01-01 00:00:00 +0000
+++ lib/lp/services/tarfile_helpers.py	2010-12-17 14:19:59 +0000
@@ -0,0 +1,108 @@
+# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Helpers to work with tar files more easily."""
+
+__metaclass__ = type
+
+__all__ = [
+    'LaunchpadWriteTarFile',
+    ]
+
+import os
+from StringIO import StringIO
+import tarfile
+import tempfile
+import time
+
+# A note about tarballs, StringIO and unicode. SQLObject returns unicode
+# values for columns which are declared as StringCol. We have to be careful
+# not to pass unicode instances to the tarfile module, because when the
+# tarfile's filehandle is a StringIO object, the StringIO object gets upset
+# later when we ask it for its value and it tries to join together its
+# buffers. This is why the tarball code is sprinkled with ".encode('ascii')".
+# If we get separate StringCol and UnicodeCol column types, we won't need this
+# any longer.
+# -- Dafydd Harries, 2005-04-07.
+
+class LaunchpadWriteTarFile:
+    """Convenience wrapper around the tarfile module.
+
+    This class makes it convenient to generate tar files in various ways.
+    """
+
+    def __init__(self, stream):
+        self.tarfile = tarfile.open('', 'w:gz', stream)
+        self.closed = False
+
+    @classmethod
+    def files_to_stream(cls, files):
+        """Turn a dictionary of files into a data stream."""
+        buffer = tempfile.TemporaryFile()
+        archive = cls(buffer)
+        archive.add_files(files)
+        archive.close()
+        buffer.seek(0)
+        return buffer
+
+    @classmethod
+    def files_to_string(cls, files):
+        """Turn a dictionary of files into a data string."""
+        return cls.files_to_stream(files).read()
+
+    @classmethod
+    def files_to_tarfile(cls, files):
+        """Turn a dictionary of files into a tarfile object."""
+        return tarfile.open('', 'r', cls.files_to_stream(files))
+
+    def close(self):
+        """Close the archive.
+
+        After the archive is closed, the data written to the filehandle will
+        be complete. The archive may not be appended to after it has been
+        closed.
+        """
+
+        self.tarfile.close()
+        self.closed = True
+
+    def add_file(self, path, contents):
+        """Add a file to the archive."""
+        assert not self.closed, "Can't add a file to a closed archive"
+
+        now = int(time.time())
+        path_bits = path.split(os.path.sep)
+
+        # Ensure that all the directories in the path are present in the
+        # archive.
+        for i in range(1, len(path_bits)):
+            joined_path = os.path.join(*path_bits[:i])
+
+            try:
+                self.tarfile.getmember(joined_path)
+            except KeyError:
+                tarinfo = tarfile.TarInfo(joined_path)
+                tarinfo.type = tarfile.DIRTYPE
+                tarinfo.mtime = now
+                tarinfo.mode = 0755
+                tarinfo.uname = 'launchpad'
+                tarinfo.gname = 'launchpad'
+                self.tarfile.addfile(tarinfo)
+
+        tarinfo = tarfile.TarInfo(path)
+        tarinfo.time = now
+        tarinfo.mtime = now
+        tarinfo.mode = 0644
+        tarinfo.size = len(contents)
+        tarinfo.uname = 'launchpad'
+        tarinfo.gname = 'launchpad'
+        self.tarfile.addfile(tarinfo, StringIO(contents))
+
+    def add_files(self, files):
+        """Add a number of files to the archive.
+
+        :param files: A dictionary mapping file names to file contents.
+        """
+
+        for filename in sorted(files.keys()):
+            self.add_file(filename, files[filename])

=== modified file 'lib/lp/translations/doc/translationimportqueue.txt'
--- lib/lp/translations/doc/translationimportqueue.txt	2010-12-01 11:26:57 +0000
+++ lib/lp/translations/doc/translationimportqueue.txt	2010-12-17 14:19:59 +0000
@@ -1103,8 +1103,7 @@
 addOrUpdateEntry adds a new entry to the import queue so we can handle it
 later with poimport script.
 
-    >>> from lp.translations.utilities.translation_export import (
-    ...     LaunchpadWriteTarFile)
+    >>> from lp.services.tarfile_helpers import LaunchpadWriteTarFile
     >>> potemplate_set = getUtility(IPOTemplateSet)
     >>> potemplate_subset = potemplate_set.getSubset(
     ...     productseries=evolution_productseries)

=== modified file 'lib/lp/translations/model/translationimportqueue.py'
--- lib/lp/translations/model/translationimportqueue.py	2010-12-01 11:26:57 +0000
+++ lib/lp/translations/model/translationimportqueue.py	2010-12-17 14:19:59 +0000
@@ -992,7 +992,7 @@
 
     def _makePath(self, name, path_filter):
         """Make the file path from the name stored in the tarball."""
-        path = posixpath.normpath(name)
+        path = posixpath.normpath(name).lstrip('/')
         if path_filter:
             path = path_filter(path)
         return path

=== modified file 'lib/lp/translations/scripts/language_pack.py'
--- lib/lp/translations/scripts/language_pack.py	2010-08-20 20:31:18 +0000
+++ lib/lp/translations/scripts/language_pack.py	2010-12-17 14:19:59 +0000
@@ -39,7 +39,7 @@
     TranslationFileFormat,
     )
 from lp.translations.interfaces.vpoexport import IVPOExportSet
-from lp.translations.utilities.translation_export import LaunchpadWriteTarFile
+from lp.services.tarfile_helpers import LaunchpadWriteTarFile
 
 
 def iter_sourcepackage_translationdomain_mapping(series):

=== modified file 'lib/lp/translations/tests/test_translationimportqueue.py'
--- lib/lp/translations/tests/test_translationimportqueue.py	2010-11-03 06:14:23 +0000
+++ lib/lp/translations/tests/test_translationimportqueue.py	2010-12-17 14:19:59 +0000
@@ -3,6 +3,8 @@
 
 __metaclass__ = type
 
+import posixpath
+
 import transaction
 from zope.component import getUtility
 
@@ -11,6 +13,7 @@
     LaunchpadFunctionalLayer,
     LaunchpadZopelessLayer,
     )
+from lp.services.tarfile_helpers import LaunchpadWriteTarFile
 from lp.services.worlddata.interfaces.language import ILanguageSet
 from lp.testing import (
     person_logged_in,
@@ -330,3 +333,78 @@
                 productseries=self.product.series[0])
             self.product.owner = self.new_owner
         self.assertEqual(self.old_owner, old_entry.importer)
+
+
+class TestTranslationImportQueue(TestCaseWithFactory):
+    """Tests for `TranslationImportQueue`."""
+
+    layer = LaunchpadZopelessLayer
+
+    def setUp(self):
+        super(TestTranslationImportQueue, self).setUp()
+        self.productseries = self.factory.makeProductSeries()
+        self.importer = self.factory.makePerson()
+        self.import_queue = getUtility(ITranslationImportQueue)
+
+    def _makeFile(self, extension=None, directory=None):
+        """Create a file with arbitrary name and content.
+
+        Returns a tuple (name, content).
+        """
+        filename = self.factory.getUniqueString()
+        if extension is not None:
+            filename = "%s.%s" % (filename, extension)
+        if directory is not None:
+            filename = posixpath.join(directory, filename)
+        content = self.factory.getUniqueString()
+        return (filename, content)
+
+    def _getQueuePaths(self):
+        entries = self.import_queue.getAllEntries(target=self.productseries)
+        return [entry.path for entry in entries]
+
+    def test_addOrUpdateEntriesFromTarball_baseline(self):
+        # Files from a tarball are placed in the queue.
+        files = dict((
+            self._makeFile('pot'),
+            self._makeFile('po'),
+            self._makeFile('xpi'),
+            ))
+        tarfile_content = LaunchpadWriteTarFile.files_to_string(files)
+        self.import_queue.addOrUpdateEntriesFromTarball(
+            tarfile_content, True, self.importer,
+            productseries=self.productseries)
+        self.assertContentEqual(files.keys(), self._getQueuePaths())
+
+    def test_addOrUpdateEntriesFromTarball_only_translation_files(self):
+        # Only files with the right extensions are added.
+        files = dict((
+            self._makeFile(),
+            ))
+        tarfile_content = LaunchpadWriteTarFile.files_to_string(files)
+        self.import_queue.addOrUpdateEntriesFromTarball(
+            tarfile_content, True, self.importer,
+            productseries=self.productseries)
+        self.assertEqual([], self._getQueuePaths())
+
+    def test_addOrUpdateEntriesFromTarball_path(self):
+        # File names are store with full path.
+        files = dict((
+            self._makeFile('pot', 'directory'),
+            ))
+        tarfile_content = LaunchpadWriteTarFile.files_to_string(files)
+        self.import_queue.addOrUpdateEntriesFromTarball(
+            tarfile_content, True, self.importer,
+            productseries=self.productseries)
+        self.assertEqual(files.keys(), self._getQueuePaths())
+
+    def test_addOrUpdateEntriesFromTarball_path_leading_slash(self):
+        # Leading slashes are stripped from path names.
+        path, content = self._makeFile('pot', '/directory')
+        files = dict(((path, content),))
+        tarfile_content = LaunchpadWriteTarFile.files_to_string(files)
+        self.import_queue.addOrUpdateEntriesFromTarball(
+            tarfile_content, True, self.importer,
+            productseries=self.productseries)
+        stripped_path = path.lstrip('/')
+        self.assertEqual([stripped_path], self._getQueuePaths())

=== modified file 'lib/lp/translations/utilities/translation_export.py'
--- lib/lp/translations/utilities/translation_export.py	2010-08-20 20:31:18 +0000
+++ lib/lp/translations/utilities/translation_export.py	2010-12-17 14:19:59 +0000
@@ -9,18 +9,15 @@
     'ExportedTranslationFile',
     'ExportFileStorage',
     'TranslationExporter',
-    'LaunchpadWriteTarFile',
     ]
 
-import os
 from StringIO import StringIO
-import tarfile
 import tempfile
-import time
 
 from zope.component import subscribers
 from zope.interface import implements
 
+from lp.services.tarfile_helpers import LaunchpadWriteTarFile
 from lp.translations.interfaces.translationexporter import (
     IExportedTranslationFile,
     ITranslationExporter,
@@ -101,99 +98,6 @@
         return storage.export()
 
 
-# A note about tarballs, StringIO and unicode. SQLObject returns unicode
-# values for columns which are declared as StringCol. We have to be careful
-# not to pass unicode instances to the tarfile module, because when the
-# tarfile's filehandle is a StringIO object, the StringIO object gets upset
-# later when we ask it for its value and it tries to join together its
-# buffers. This is why the tarball code is sprinkled with ".encode('ascii')".
-# If we get separate StringCol and UnicodeCol column types, we won't need this
-# any longer.
-# -- Dafydd Harries, 2005-04-07.
-
-class LaunchpadWriteTarFile:
-    """Convenience wrapper around the tarfile module.
-
-    This class makes it convenient to generate tar files in various ways.
-    """
-
-    def __init__(self, stream):
-        self.tarfile = tarfile.open('', 'w:gz', stream)
-        self.closed = False
-
-    @classmethod
-    def files_to_stream(cls, files):
-        """Turn a dictionary of files into a data stream."""
-        buffer = tempfile.TemporaryFile()
-        archive = cls(buffer)
-        archive.add_files(files)
-        archive.close()
-        buffer.seek(0)
-        return buffer
-
-    @classmethod
-    def files_to_string(cls, files):
-        """Turn a dictionary of files into a data string."""
-        return cls.files_to_stream(files).read()
-
-    @classmethod
-    def files_to_tarfile(cls, files):
-        """Turn a dictionary of files into a tarfile object."""
-        return tarfile.open('', 'r', cls.files_to_stream(files))
-
-    def close(self):
-        """Close the archive.
-
-        After the archive is closed, the data written to the filehandle will
-        be complete. The archive may not be appended to after it has been
-        closed.
-        """
-
-        self.tarfile.close()
-        self.closed = True
-
-    def add_file(self, path, contents):
-        """Add a file to the archive."""
-        assert not self.closed, "Can't add a file to a closed archive"
-
-        now = int(time.time())
-        path_bits = path.split(os.path.sep)
-
-        # Ensure that all the directories in the path are present in the
-        # archive.
-        for i in range(1, len(path_bits)):
-            joined_path = os.path.join(*path_bits[:i])
-
-            try:
-                self.tarfile.getmember(joined_path)
-            except KeyError:
-                tarinfo = tarfile.TarInfo(joined_path)
-                tarinfo.type = tarfile.DIRTYPE
-                tarinfo.mtime = now
-                tarinfo.mode = 0755
-                tarinfo.uname = 'launchpad'
-                tarinfo.gname = 'launchpad'
-                self.tarfile.addfile(tarinfo)
-
-        tarinfo = tarfile.TarInfo(path)
-        tarinfo.time = now
-        tarinfo.mtime = now
-        tarinfo.mode = 0644
-        tarinfo.size = len(contents)
-        tarinfo.uname = 'launchpad'
-        tarinfo.gname = 'launchpad'
-        self.tarfile.addfile(tarinfo, StringIO(contents))
-
-    def add_files(self, files):
-        """Add a number of files to the archive.
-
-        :param files: A dictionary mapping file names to file contents.
-        """
-
-        for filename in sorted(files.keys()):
-            self.add_file(filename, files[filename])
-
-
 class StorageStrategy:
     """Implementation strategy for `ExportFileStorage`.
 


Follow ups