From 4f46e5a323d172be690e809e48db63c35152d665 Mon Sep 17 00:00:00 2001 From: Teika Kazura Date: Sun, 23 Feb 2025 13:59:55 +0900 Subject: [PATCH 1/2] YoutubeDL.py: Defines a new function validate_destination_filename(), which checks the filename length limit before downloading. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This closes issue #8908 """yt-dlp should abort on first "File name too long" error""". Currenty the test fails at test/test_YoutubeDL.py:819. The message is: ------------------------------------------------------------------------- TestYoutubeDL::test_prepare_outtmpl_and_filename - OSError: [Errno 36] File name too long: '{"id ": "1234", "ext": "mp4", "width": null,... ------------------------------------------------------------------------- And the tried filename is: ------------------------------------------------------------------------ '{"id": "1234", "ext": "mp4", "width": null, "height": 1080, "filesize": 1024, "title1": "$PATH", "title2": "%PATH%", "title3": "foo⧸bar⧹⧹test", "title4": "foo ⧹"ba⧹" test", "title5": "⧹u00e1⧹u00e9⧹u00ed ⧹ud835⧹udc00", "timestamp": 1618488000, "duration: 100000, "playlist_index": 1, "playlist_autonumber": 2, "__last_playlist_index": 100, "n_entries": 10, "formats": [{"id": "id 1", "height": 1080, "width": 1920}, {"id": "id2", "height": 720}, {"id": "id 3"}], "epoch": 1740284668, "duration_string": "27-46-40", "autonumber": 1, "video_autonumber": 0, "resolution": "1080p"}' ------------------------------------------------------------------------ --- yt_dlp/YoutubeDL.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/yt_dlp/YoutubeDL.py b/yt_dlp/YoutubeDL.py index 8790b326b7..1024911c31 100644 --- a/yt_dlp/YoutubeDL.py +++ b/yt_dlp/YoutubeDL.py @@ -1475,12 +1475,41 @@ def _prepare_filename(self, info_dict, *, outtmpl=None, tmpl_type=None): self.report_error('Error in output template: ' + str(err) + ' (encoding: ' + repr(preferredencoding()) + ')') return None + def validate_destination_filename(self, filename): + """Check if the destination filename is valid in the OS. In particular +it checks if the length does not exceed the OS limit. However currently +any error is simply raised, independent of the cause. + +Without this, download would fail only after the entire file is downloaded.""" + cwd = os.getcwd() + with tempfile.TemporaryDirectory() as d: + os.chdir(d) + try: + with open(filename, 'w') as f: + f.close() + except OSError as e: + if (os.name == 'nt' and e.errno == 206) or (e.errno == errno.ENAMETOOLONG): + # The first condition is for windows, + # and the second for unix-ish systems. + + # An improvement idea: + # by default, retry (exec yt-dlp itself) by + # -o "%(id)s.%(ext)s" --write-info-json, + # but respect the directory from --output of the original call. + self.to_screen('''[Notice] The file name to be saved is too long, exceeding the OS limit. +[Notice] Consider options --trim-filenames or -o (--output).''') + + raise + finally: + os.chdir(cwd) + def prepare_filename(self, info_dict, dir_type='', *, outtmpl=None, warn=False): """Generate the output filename""" if outtmpl: assert not dir_type, 'outtmpl and dir_type are mutually exclusive' dir_type = None filename = self._prepare_filename(info_dict, tmpl_type=dir_type, outtmpl=outtmpl) + self.validate_destination_filename(filename) if not filename and dir_type not in ('', 'temp'): return '' From 52d0130a37f144b1b5b7bccb8c99945d068bee78 Mon Sep 17 00:00:00 2001 From: Teika Kazura Date: Tue, 11 Mar 2025 18:21:47 +0900 Subject: [PATCH 2/2] * YoutubeDL:validate_destination_filename(): Bugfix of the previous commit when -o contains a directory. * test_YoutubeDL:test_prepare_outtmpl_and_filename(): Prevents bogus test failure. --- test/test_YoutubeDL.py | 1 + yt_dlp/YoutubeDL.py | 47 +++++++++++++++++++++++++++--------------- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/test/test_YoutubeDL.py b/test/test_YoutubeDL.py index 708a04f92d..c742e4eb4e 100644 --- a/test/test_YoutubeDL.py +++ b/test/test_YoutubeDL.py @@ -742,6 +742,7 @@ def test(tmpl, expected, *, info=None, **params): params['outtmpl'] = tmpl ydl = FakeYDL(params) ydl._num_downloads = 1 + ydl.validate_destination_filename = lambda *args: None self.assertEqual(ydl.validate_outtmpl(tmpl), None) out = ydl.evaluate_outtmpl(tmpl, info or self.outtmpl_info) diff --git a/yt_dlp/YoutubeDL.py b/yt_dlp/YoutubeDL.py index 1024911c31..44f943800d 100644 --- a/yt_dlp/YoutubeDL.py +++ b/yt_dlp/YoutubeDL.py @@ -12,6 +12,7 @@ import locale import operator import os +import pathlib import random import re import shutil @@ -1481,27 +1482,39 @@ def validate_destination_filename(self, filename): any error is simply raised, independent of the cause. Without this, download would fail only after the entire file is downloaded.""" - cwd = os.getcwd() - with tempfile.TemporaryDirectory() as d: - os.chdir(d) - try: - with open(filename, 'w') as f: - f.close() - except OSError as e: - if (os.name == 'nt' and e.errno == 206) or (e.errno == errno.ENAMETOOLONG): - # The first condition is for windows, - # and the second for unix-ish systems. + # An improvement idea: + # by default, retry (exec yt-dlp itself) by + # -o "%(id)s.%(ext)s" --write-info-json, + # but respect the directory from --output of the original call. - # An improvement idea: - # by default, retry (exec yt-dlp itself) by - # -o "%(id)s.%(ext)s" --write-info-json, - # but respect the directory from --output of the original call. + with tempfile.TemporaryDirectory() as d: + try: + # To make sure it's confined under tmpdir + tmpfn = '.' + os.sep + os.path.splitdrive(filename)[1] + + parentDirStr = os.sep + '..' + os.sep + # This may contain '../' so remove them. + while parentDirStr in tmpfn: + tmpfn = tmpfn.replace(parentDirStr, os.sep) + tmpfn = os.path.join(d, tmpfn) + pathlib.Path(os.path.dirname(tmpfn)).mkdir(parents=True, exist_ok=True) + open(tmpfn, 'w').close() + except OSError as e: + if (os.name == 'nt' and e.errno == 206) or (os.name != 'nt' and e.errno == errno.ENAMETOOLONG): + # For Win, 206 means filename length exceeds MAX_PATH. + + e.filename = filename self.to_screen('''[Notice] The file name to be saved is too long, exceeding the OS limit. [Notice] Consider options --trim-filenames or -o (--output).''') - raise - finally: - os.chdir(cwd) + elif os.name == 'nt' and e.errno == 22: + # Even when MAX_PATH is disabled, 255 chars is the limit, resulting in 22. + # https://github.com/python/cpython/issues/126929#issuecomment-2483684861 + e.filename = filename + self.to_screen(f'''[Notice] Attempt to create file {filename} resulted in Errno 22. +This is often caused e.g. by too long filename or forbidden characters.''') + + raise def prepare_filename(self, info_dict, dir_type='', *, outtmpl=None, warn=False): """Generate the output filename"""