Merge pull request #8739 from remitamine/update_url_params
[youtube-dl] / youtube_dl / postprocessor / xattrpp.py
index ac2236f356af9005ec8d30f69a8419a3e9010841..e39ca60aa08326b6f05814ff800bb09c75755e48 100644 (file)
@@ -1,14 +1,37 @@
+from __future__ import unicode_literals
+
 import os
 import subprocess
 import sys
+import errno
 
 from .common import PostProcessor
+from ..compat import compat_os_name
 from ..utils import (
+    check_executable,
     hyphenate_date,
-    preferredencoding,
+    version_tuple,
+    PostProcessingError,
+    encodeArgument,
+    encodeFilename,
 )
 
 
+class XAttrMetadataError(PostProcessingError):
+    def __init__(self, code=None, msg='Unknown error'):
+        super(XAttrMetadataError, self).__init__(msg)
+        self.code = code
+
+        # Parsing code and msg
+        if (self.code in (errno.ENOSPC, errno.EDQUOT) or
+                'No space left' in self.msg or 'Disk quota excedded' in self.msg):
+            self.reason = 'NO_SPACE'
+        elif self.code == errno.E2BIG or 'Argument list too long' in self.msg:
+            self.reason = 'VALUE_TOO_LONG'
+        else:
+            self.reason = 'NOT_SUPPORTED'
+
+
 class XAttrMetadataPP(PostProcessor):
 
     #
@@ -30,48 +53,68 @@ class XAttrMetadataPP(PostProcessor):
         try:
             # try the pyxattr module...
             import xattr
-            def write_xattr(path, key, value):
-                return xattr.setxattr(path, key, value)
 
-        except ImportError:
+            # Unicode arguments are not supported in python-pyxattr until
+            # version 0.5.0
+            # See https://github.com/rg3/youtube-dl/issues/5498
+            pyxattr_required_version = '0.5.0'
+            if version_tuple(xattr.__version__) < version_tuple(pyxattr_required_version):
+                self._downloader.report_warning(
+                    'python-pyxattr is detected but is too old. '
+                    'youtube-dl requires %s or above while your version is %s. '
+                    'Falling back to other xattr implementations' % (
+                        pyxattr_required_version, xattr.__version__))
 
-            if os.name == 'posix':
-                def which(bin):
-                    for dir in os.environ["PATH"].split(":"):
-                        path = os.path.join(dir, bin)
-                        if os.path.exists(path):
-                            return path
+                raise ImportError
 
-                user_has_setfattr = which("setfattr")
-                user_has_xattr    = which("xattr")
+            def write_xattr(path, key, value):
+                try:
+                    xattr.set(path, key, value)
+                except EnvironmentError as e:
+                    raise XAttrMetadataError(e.errno, e.strerror)
+
+        except ImportError:
+            if compat_os_name == 'nt':
+                # Write xattrs to NTFS Alternate Data Streams:
+                # http://en.wikipedia.org/wiki/NTFS#Alternate_data_streams_.28ADS.29
+                def write_xattr(path, key, value):
+                    assert ':' not in key
+                    assert os.path.exists(path)
+
+                    ads_fn = path + ':' + key
+                    try:
+                        with open(ads_fn, 'wb') as f:
+                            f.write(value)
+                    except EnvironmentError as e:
+                        raise XAttrMetadataError(e.errno, e.strerror)
+            else:
+                user_has_setfattr = check_executable('setfattr', ['--version'])
+                user_has_xattr = check_executable('xattr', ['-h'])
 
                 if user_has_setfattr or user_has_xattr:
 
                     def write_xattr(path, key, value):
-                        import errno
-                        potential_errors = {
-                            # setfattr: /tmp/blah: Operation not supported
-                            "Operation not supported": errno.EOPNOTSUPP,
-                            # setfattr: ~/blah: No such file or directory
-                            # xattr: No such file: ~/blah
-                            "No such file": errno.ENOENT,
-                        }
-
+                        value = value.decode('utf-8')
                         if user_has_setfattr:
-                            cmd = ['setfattr', '-n', key, '-v', value, path]
+                            executable = 'setfattr'
+                            opts = ['-n', key, '-v', value]
                         elif user_has_xattr:
-                            cmd = ['xattr', '-w', key, value, path]
+                            executable = 'xattr'
+                            opts = ['-w', key, value]
+
+                        cmd = ([encodeFilename(executable, True)] +
+                               [encodeArgument(o) for o in opts] +
+                               [encodeFilename(path, True)])
 
                         try:
-                            subprocess.check_output(cmd, stderr=subprocess.STDOUT)
-                        except subprocess.CalledProcessError as e:
-                            errorstr = e.output.strip().decode()
-                            for potential_errorstr, potential_errno in potential_errors.items():
-                                if errorstr.find(potential_errorstr) > -1:
-                                    e = OSError(potential_errno, potential_errorstr)
-                                    e.__cause__ = None
-                                    raise e
-                            raise  # Reraise unhandled error
+                            p = subprocess.Popen(
+                                cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
+                        except EnvironmentError as e:
+                            raise XAttrMetadataError(e.errno, e.strerror)
+                        stdout, stderr = p.communicate()
+                        stderr = stderr.decode('utf-8', 'replace')
+                        if p.returncode != 0:
+                            raise XAttrMetadataError(p.returncode, stderr)
 
                 else:
                     # On Unix, and can't find pyxattr, setfattr, or xattr.
@@ -86,19 +129,9 @@ class XAttrMetadataPP(PostProcessor):
                             "Couldn't find a tool to set the xattrs. "
                             "Install either the python 'xattr' module, "
                             "or the 'xattr' binary.")
-            else:
-                # Write xattrs to NTFS Alternate Data Streams: http://en.wikipedia.org/wiki/NTFS#Alternate_data_streams_.28ADS.29
-                def write_xattr(path, key, value):
-                    assert(key.find(":") < 0)
-                    assert(path.find(":") < 0)
-                    assert(os.path.exists(path))
-
-                    ads_fn = path + ":" + key
-                    with open(ads_fn, "w") as f:
-                        f.write(value)
 
         # Write the metadata to the file's xattrs
-        self._downloader.to_screen('[metadata] Writing metadata to file\'s xattrs...')
+        self._downloader.to_screen('[metadata] Writing metadata to file\'s xattrs')
 
         filename = info['filepath']
 
@@ -118,15 +151,27 @@ class XAttrMetadataPP(PostProcessor):
                 value = info.get(infoname)
 
                 if value:
-                    if infoname == "upload_date":
+                    if infoname == 'upload_date':
                         value = hyphenate_date(value)
 
-                    byte_value = value.encode(preferredencoding())
+                    byte_value = value.encode('utf-8')
                     write_xattr(filename, xattrname, byte_value)
 
-            return True, info
-
-        except OSError:
-            self._downloader.report_error("This filesystem doesn't support extended attributes. (You may have to enable them in your /etc/fstab)")
-            return False, info
+            return [], info
 
+        except XAttrMetadataError as e:
+            if e.reason == 'NO_SPACE':
+                self._downloader.report_warning(
+                    'There\'s no disk space left or disk quota exceeded. ' +
+                    'Extended attributes are not written.')
+            elif e.reason == 'VALUE_TOO_LONG':
+                self._downloader.report_warning(
+                    'Unable to write extended attributes due to too long values.')
+            else:
+                msg = 'This filesystem doesn\'t support extended attributes. '
+                if compat_os_name == 'nt':
+                    msg += 'You need to use NTFS.'
+                else:
+                    msg += '(You may have to enable them in your /etc/fstab)'
+                self._downloader.report_error(msg)
+            return [], info