[xattr] Enhance error handling to catch ENOSPC
[youtube-dl] / youtube_dl / postprocessor / xattrpp.py
1 from __future__ import unicode_literals
2
3 import os
4 import subprocess
5 import sys
6 import errno
7
8 from .common import PostProcessor
9 from ..utils import (
10     check_executable,
11     hyphenate_date,
12     version_tuple,
13     PostProcessingError,
14     encodeArgument,
15     encodeFilename,
16 )
17
18
19 class XAttrMetadataError(PostProcessingError):
20     def __init__(self, code=None, msg='Unknown error'):
21         super(XAttrMetadataError, self).__init__(msg)
22         self.code = code
23
24         # Parsing code and msg
25         if (self.code in (errno.ENOSPC, errno.EDQUOT) or
26                 'No space left' in self.msg or 'Disk quota excedded' in self.msg):
27             self.reason = 'NO_SPACE'
28         else:
29             self.reason = 'NOT_SUPPORTED'
30
31
32 class XAttrMetadataPP(PostProcessor):
33
34     #
35     # More info about extended attributes for media:
36     #   http://freedesktop.org/wiki/CommonExtendedAttributes/
37     #   http://www.freedesktop.org/wiki/PhreedomDraft/
38     #   http://dublincore.org/documents/usageguide/elements.shtml
39     #
40     # TODO:
41     #  * capture youtube keywords and put them in 'user.dublincore.subject' (comma-separated)
42     #  * figure out which xattrs can be used for 'duration', 'thumbnail', 'resolution'
43     #
44
45     def run(self, info):
46         """ Set extended attributes on downloaded file (if xattr support is found). """
47
48         # This mess below finds the best xattr tool for the job and creates a
49         # "write_xattr" function.
50         try:
51             # try the pyxattr module...
52             import xattr
53
54             # Unicode arguments are not supported in python-pyxattr until
55             # version 0.5.0
56             # See https://github.com/rg3/youtube-dl/issues/5498
57             pyxattr_required_version = '0.5.0'
58             if version_tuple(xattr.__version__) < version_tuple(pyxattr_required_version):
59                 self._downloader.report_warning(
60                     'python-pyxattr is detected but is too old. '
61                     'youtube-dl requires %s or above while your version is %s. '
62                     'Falling back to other xattr implementations' % (
63                         pyxattr_required_version, xattr.__version__))
64
65                 raise ImportError
66
67             def write_xattr(path, key, value):
68                 try:
69                     xattr.set(path, key, value)
70                 except EnvironmentError as e:
71                     raise XAttrMetadataError(e.errno, e.strerror)
72
73         except ImportError:
74             if os.name == 'nt':
75                 # Write xattrs to NTFS Alternate Data Streams:
76                 # http://en.wikipedia.org/wiki/NTFS#Alternate_data_streams_.28ADS.29
77                 def write_xattr(path, key, value):
78                     assert ':' not in key
79                     assert os.path.exists(path)
80
81                     ads_fn = path + ":" + key
82                     try:
83                         with open(ads_fn, "wb") as f:
84                             f.write(value)
85                     except EnvironmentError as e:
86                         raise XAttrMetadataError(e.errno, e.strerror)
87             else:
88                 user_has_setfattr = check_executable("setfattr", ['--version'])
89                 user_has_xattr = check_executable("xattr", ['-h'])
90
91                 if user_has_setfattr or user_has_xattr:
92
93                     def write_xattr(path, key, value):
94                         value = value.decode('utf-8')
95                         if user_has_setfattr:
96                             executable = 'setfattr'
97                             opts = ['-n', key, '-v', value]
98                         elif user_has_xattr:
99                             executable = 'xattr'
100                             opts = ['-w', key, value]
101
102                         cmd = ([encodeFilename(executable, True)] +
103                                [encodeArgument(o) for o in opts] +
104                                [encodeFilename(path, True)])
105
106                         p = subprocess.Popen(
107                             cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
108                         stdout, stderr = p.communicate()
109                         stderr = stderr.decode('utf-8', 'replace')
110                         if p.returncode != 0:
111                             raise XAttrMetadataError(p.returncode, stderr)
112
113                 else:
114                     # On Unix, and can't find pyxattr, setfattr, or xattr.
115                     if sys.platform.startswith('linux'):
116                         self._downloader.report_error(
117                             "Couldn't find a tool to set the xattrs. "
118                             "Install either the python 'pyxattr' or 'xattr' "
119                             "modules, or the GNU 'attr' package "
120                             "(which contains the 'setfattr' tool).")
121                     else:
122                         self._downloader.report_error(
123                             "Couldn't find a tool to set the xattrs. "
124                             "Install either the python 'xattr' module, "
125                             "or the 'xattr' binary.")
126
127         # Write the metadata to the file's xattrs
128         self._downloader.to_screen('[metadata] Writing metadata to file\'s xattrs')
129
130         filename = info['filepath']
131
132         try:
133             xattr_mapping = {
134                 'user.xdg.referrer.url': 'webpage_url',
135                 # 'user.xdg.comment':            'description',
136                 'user.dublincore.title': 'title',
137                 'user.dublincore.date': 'upload_date',
138                 'user.dublincore.description': 'description',
139                 'user.dublincore.contributor': 'uploader',
140                 'user.dublincore.format': 'format',
141             }
142
143             for xattrname, infoname in xattr_mapping.items():
144
145                 value = info.get(infoname)
146
147                 if value:
148                     if infoname == "upload_date":
149                         value = hyphenate_date(value)
150
151                     byte_value = value.encode('utf-8')
152                     write_xattr(filename, xattrname, byte_value)
153
154             return [], info
155
156         except XAttrMetadataError as e:
157             if e.reason == 'NO_SPACE':
158                 self._downloader.report_warning(
159                     'There\'s no disk space left or disk quota exceeded. ' +
160                     'Extended attributes are not written.')
161             else:
162                 self._downloader.report_error(
163                     'This filesystem doesn\'t support extended attributes. ' +
164                     '(You may have to enable them in your /etc/fstab)')
165             return [], info