[BUGFIX] Fix "File name too long" error when a title is used as a folder name (#1479)
Some checks failed
ytld-sub CI (Linux) / test-lint (push) Has been cancelled
ytld-sub CI (Linux) / test-unit (push) Has been cancelled
ytld-sub CI (Windows) / test-unit (push) Has been cancelled
ytld-sub CI (Windows) / test-integration (push) Has been cancelled
ytld-sub CI (Windows) / test-integration-prebuilt-presets (push) Has been cancelled
ytld-sub CI (Windows) / test-e2e (push) Has been cancelled
ytld-sub CI (Linux) / test-integration (push) Has been cancelled
ytld-sub CI (Linux) / test-integration-prebuilt-presets (push) Has been cancelled
ytld-sub CI (Linux) / test-e2e (push) Has been cancelled
ytld-sub Docker GUI Build / version (push) Has been cancelled
ytld-sub Docker GUI Build / build (3.12) (push) Has been cancelled
ytld-sub Docker Ubuntu Build / version (push) Has been cancelled
ytld-sub Docker Ubuntu Build / build (3.12) (push) Has been cancelled
ytld-sub Docker Build / version (push) Has been cancelled
ytld-sub Docker Build / build (3.12) (push) Has been cancelled
ytld-sub Release / version (push) Has been cancelled
ytld-sub CI (Linux) / codecov-upload (push) Has been cancelled
ytld-sub Docker GUI Build / package-arm64 (push) Has been cancelled
ytld-sub Docker GUI Build / package-amd64 (push) Has been cancelled
ytld-sub Docker GUI Build / deploy (push) Has been cancelled
ytld-sub Docker Ubuntu Build / package-arm64 (push) Has been cancelled
ytld-sub Docker Ubuntu Build / package-amd64 (push) Has been cancelled
ytld-sub Docker Ubuntu Build / deploy (push) Has been cancelled
ytld-sub Docker Build / package-arm64 (push) Has been cancelled
ytld-sub Docker Build / package-amd64 (push) Has been cancelled
ytld-sub Docker Build / deploy (push) Has been cancelled
ytld-sub Release / build-linux (push) Has been cancelled
ytld-sub Release / build-windows (push) Has been cancelled
ytld-sub Release / github-release (push) Has been cancelled
ytld-sub Release / pypi-publish (push) Has been cancelled
Some checks failed
ytld-sub CI (Linux) / test-lint (push) Has been cancelled
ytld-sub CI (Linux) / test-unit (push) Has been cancelled
ytld-sub CI (Windows) / test-unit (push) Has been cancelled
ytld-sub CI (Windows) / test-integration (push) Has been cancelled
ytld-sub CI (Windows) / test-integration-prebuilt-presets (push) Has been cancelled
ytld-sub CI (Windows) / test-e2e (push) Has been cancelled
ytld-sub CI (Linux) / test-integration (push) Has been cancelled
ytld-sub CI (Linux) / test-integration-prebuilt-presets (push) Has been cancelled
ytld-sub CI (Linux) / test-e2e (push) Has been cancelled
ytld-sub Docker GUI Build / version (push) Has been cancelled
ytld-sub Docker GUI Build / build (3.12) (push) Has been cancelled
ytld-sub Docker Ubuntu Build / version (push) Has been cancelled
ytld-sub Docker Ubuntu Build / build (3.12) (push) Has been cancelled
ytld-sub Docker Build / version (push) Has been cancelled
ytld-sub Docker Build / build (3.12) (push) Has been cancelled
ytld-sub Release / version (push) Has been cancelled
ytld-sub CI (Linux) / codecov-upload (push) Has been cancelled
ytld-sub Docker GUI Build / package-arm64 (push) Has been cancelled
ytld-sub Docker GUI Build / package-amd64 (push) Has been cancelled
ytld-sub Docker GUI Build / deploy (push) Has been cancelled
ytld-sub Docker Ubuntu Build / package-arm64 (push) Has been cancelled
ytld-sub Docker Ubuntu Build / package-amd64 (push) Has been cancelled
ytld-sub Docker Ubuntu Build / deploy (push) Has been cancelled
ytld-sub Docker Build / package-arm64 (push) Has been cancelled
ytld-sub Docker Build / package-amd64 (push) Has been cancelled
ytld-sub Docker Build / deploy (push) Has been cancelled
ytld-sub Release / build-linux (push) Has been cancelled
ytld-sub Release / build-windows (push) Has been cancelled
ytld-sub Release / github-release (push) Has been cancelled
ytld-sub Release / pypi-publish (push) Has been cancelled
Some presets put the video title into a folder name (not just the file name). The code only shortened the file name, never the folder name. So a title with multi-byte characters (like bold/fancy Unicode, which take 4 bytes each) could make the folder name go over the OS 255-byte limit and crash with OSError: [Errno 36] File name too long. This fix shortens every folder in the path too, not just the file name, while keeping the file extension. output_directory is left alone so real folders aren't touched. Added a test with a title made of 4-byte bold Unicode characters. Relates to #1092, #1189 -- Thanks @Nojyto for the contribution!
This commit is contained in:
parent
1c4633b815
commit
658f5ef3e1
3 changed files with 72 additions and 1 deletions
|
|
@ -51,6 +51,13 @@ class FilePathTruncater:
|
||||||
|
|
||||||
return f"{file_sub_name}{delimiter}{file_ext}"
|
return f"{file_sub_name}{delimiter}{file_ext}"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _truncate_directory_name(cls, directory_name: str) -> str:
|
||||||
|
while len(directory_name.encode("utf-8")) > cls._MAX_BASE_FILE_NAME_BYTES:
|
||||||
|
directory_name = directory_name[:-1]
|
||||||
|
|
||||||
|
return directory_name
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def maybe_truncate_file_path(cls, file_path: str) -> str:
|
def maybe_truncate_file_path(cls, file_path: str) -> str:
|
||||||
"""Turn into a Path, then a string, to get correct directory separators"""
|
"""Turn into a Path, then a string, to get correct directory separators"""
|
||||||
|
|
@ -61,6 +68,30 @@ class FilePathTruncater:
|
||||||
|
|
||||||
return str(file_path)
|
return str(file_path)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def maybe_truncate_file_name_path(cls, file_name_path: str) -> str:
|
||||||
|
"""
|
||||||
|
Truncates each component of a relative output file name path. Both the
|
||||||
|
subdirectories (which can be derived from metadata such as the title) and the
|
||||||
|
final file name are truncated if they exceed the OS limit. The file name's
|
||||||
|
extension is always preserved.
|
||||||
|
"""
|
||||||
|
parts = Path(file_name_path).parts
|
||||||
|
if not parts:
|
||||||
|
return file_name_path
|
||||||
|
|
||||||
|
*directory_parts, file_name = parts
|
||||||
|
|
||||||
|
truncated_parts = [
|
||||||
|
cls._truncate_directory_name(part) if cls._is_file_name_too_long(part) else part
|
||||||
|
for part in directory_parts
|
||||||
|
]
|
||||||
|
|
||||||
|
if cls._is_file_name_too_long(file_name):
|
||||||
|
file_name = cls._truncate_file_name(file_name)
|
||||||
|
|
||||||
|
return str(Path(*truncated_parts, file_name))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def to_native_filepath(cls, file_path: str) -> str:
|
def to_native_filepath(cls, file_path: str) -> str:
|
||||||
"""Ensures file paths use the correct separator"""
|
"""Ensures file paths use the correct separator"""
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ class StringFormatterFileNameValidator(StringFormatterValidator):
|
||||||
|
|
||||||
def post_process(self, resolved: str) -> str:
|
def post_process(self, resolved: str) -> str:
|
||||||
return FilePathTruncater.to_native_filepath(
|
return FilePathTruncater.to_native_filepath(
|
||||||
FilePathTruncater.maybe_truncate_file_path(resolved)
|
FilePathTruncater.maybe_truncate_file_name_path(resolved)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,46 @@ class TestStringFormatterFilePathValidator:
|
||||||
assert len(dir_paths) == 1
|
assert len(dir_paths) == 1
|
||||||
assert Path(truncated_file_path) == dir_paths[0]
|
assert Path(truncated_file_path) == dir_paths[0]
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("ext", ["mp4", "-thumb.jpg"])
|
||||||
|
def test_truncates_subdirectory_in_file_name_path(self, ext: str):
|
||||||
|
if "thumb" not in ext: # do not put . in front of -thumb
|
||||||
|
ext = f".{ext}"
|
||||||
|
|
||||||
|
# Mathematical bold unicode characters are 4 bytes each in UTF-8, so a title used
|
||||||
|
# as a subdirectory can easily exceed the OS file name limit (typically 255 bytes).
|
||||||
|
long_directory = (
|
||||||
|
"[2026] 𝗿𝘁𝗶𝘀𝘁 𝗢𝗻𝗲 - 𝗔 𝗩𝗲𝗿𝘆 𝗟𝗼𝗻𝗴 𝗦𝗼𝗻𝗴 𝗧𝗶𝘁𝗹𝗲 "
|
||||||
|
"𝗧𝗵𝗮𝘁 𝗘𝘅𝗰𝗲𝗲𝗱𝘀 𝗧𝗵𝗲 𝗟𝗶𝗺𝗶𝘁 (𝗘𝘅𝘁𝗲𝗻𝗱𝗲𝗱 𝗠𝗶𝘅 𝘅 "
|
||||||
|
"𝗦𝗲𝗰𝗼𝗻𝗱 𝗔𝗿𝘁𝗶𝘀𝘁 & 𝗧𝗵𝗶𝗿𝗱 𝗔𝗿𝘁𝗶𝘀𝘁)"
|
||||||
|
)
|
||||||
|
# Sanity check that the directory really does exceed the limit
|
||||||
|
assert len(long_directory.encode("utf-8")) > FilePathTruncater._MAX_BASE_FILE_NAME_BYTES
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
file_path = str(Path(temp_dir) / long_directory / f"video{ext}")
|
||||||
|
|
||||||
|
formatter = StringFormatterFileNameValidator(name="test", value="")
|
||||||
|
truncated_file_path = formatter.post_process(file_path)
|
||||||
|
|
||||||
|
truncated_directory, truncated_file_name = os.path.split(truncated_file_path)
|
||||||
|
_, truncated_directory_name = os.path.split(truncated_directory)
|
||||||
|
|
||||||
|
# Both the subdirectory and the file name must fit within the OS limit
|
||||||
|
assert (
|
||||||
|
len(truncated_directory_name.encode("utf-8"))
|
||||||
|
<= FilePathTruncater._MAX_BASE_FILE_NAME_BYTES
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
len(truncated_file_name.encode("utf-8"))
|
||||||
|
<= FilePathTruncater._MAX_BASE_FILE_NAME_BYTES
|
||||||
|
)
|
||||||
|
assert truncated_file_name.endswith(ext)
|
||||||
|
|
||||||
|
# The original bug raised "OSError: [Errno 36] Filename too long" here because
|
||||||
|
# the subdirectory component was never truncated.
|
||||||
|
os.makedirs(truncated_directory, exist_ok=True)
|
||||||
|
assert os.path.isdir(truncated_directory)
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"file_name_max_bytes, expected_max",
|
"file_name_max_bytes, expected_max",
|
||||||
[
|
[
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue