From 5300b0b490b8db48fac30b5e32164be93dc574b7 Mon Sep 17 00:00:00 2001 From: Jakub Roztocil Date: Thu, 17 Mar 2016 15:58:01 +0800 Subject: [PATCH] Fixed #451 - OSError: [Errno 36] File name too long --- CHANGELOG.rst | 1 + httpie/downloads.py | 36 ++++++++++++++++++++++++++++++++++-- tests/test_downloads.py | 34 ++++++++++++++++++++++++++++++---- 3 files changed, 65 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ce50d302..23bda11a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -27,6 +27,7 @@ This project adheres to `Semantic Versioning `_. of light and dark terminals * Improved ``--debug`` output * Fixed ``--session`` when used with ``--download`` +* Fixed ``--download`` to trim too long filenames before saving the file * Fixed handling of ``Content-Type`` with multiple ``+subtype`` parts diff --git a/httpie/downloads.py b/httpie/downloads.py index b49e3352..972151e1 100644 --- a/httpie/downloads.py +++ b/httpie/downloads.py @@ -7,6 +7,7 @@ from __future__ import division import os import re import sys +import errno import mimetypes import threading from time import sleep, time @@ -135,12 +136,43 @@ def filename_from_url(url, content_type): return fn +def trim_filename(filename, max_len): + if len(filename) > max_len: + trim_by = len(filename) - max_len + name, ext = os.path.splitext(filename) + if trim_by >= len(name): + filename = filename[:-trim_by] + else: + filename = name[:-trim_by] + ext + return filename + + +def get_filename_max_length(directory): + try: + max_len = os.pathconf(directory, 'PC_NAME_MAX') + except OSError as e: + if e.errno == errno.EINVAL: + max_len = 255 + else: + raise + return max_len + + +def trim_filename_if_needed(filename, directory='.', extra=0): + max_len = get_filename_max_length(directory) - extra + if len(filename) > max_len: + filename = trim_filename(filename, max_len) + return filename + + def get_unique_filename(filename, exists=os.path.exists): attempt = 0 while True: suffix = '-' + str(attempt) if attempt > 0 else '' - if not exists(filename + suffix): - return filename + suffix + try_filename = trim_filename_if_needed(filename, extra=len(suffix)) + try_filename += suffix + if not exists(try_filename): + return try_filename attempt += 1 diff --git a/tests/test_downloads.py b/tests/test_downloads.py index 2d973c33..e09bdde3 100644 --- a/tests/test_downloads.py +++ b/tests/test_downloads.py @@ -2,6 +2,7 @@ import os import time import pytest +import mock from requests.structures import CaseInsensitiveDict from httpie.compat import urlopen @@ -74,7 +75,31 @@ class TestDownloadUtils: content_type='x-foo/bar' ) - def test_unique_filename(self): + @pytest.mark.parametrize( + 'orig_name, unique_on_attempt, expected', + [ + # Simple + ('foo.bar', 0, 'foo.bar'), + ('foo.bar', 1, 'foo.bar-1'), + ('foo.bar', 10, 'foo.bar-10'), + # Trim + ('A' * 20, 0, 'A' * 10), + ('A' * 20, 1, 'A' * 8 + '-1'), + ('A' * 20, 10, 'A' * 7 + '-10'), + # Trim before ext + ('A' * 20 + '.txt', 0, 'A' * 6 + '.txt'), + ('A' * 20 + '.txt', 1, 'A' * 4 + '.txt-1'), + # Trim at the end + ('foo.' + 'A' * 20, 0, 'foo.' + 'A' * 6), + ('foo.' + 'A' * 20, 1, 'foo.' + 'A' * 4 + '-1'), + ('foo.' + 'A' * 20, 10, 'foo.' + 'A' * 3 + '-10'), + ] + ) + @mock.patch('httpie.downloads.get_filename_max_length') + def test_unique_filename(self, get_filename_max_length, + orig_name, unique_on_attempt, + expected): + def attempts(unique_on_attempt=0): # noinspection PyUnresolvedReferences,PyUnusedLocal def exists(filename): @@ -86,9 +111,10 @@ class TestDownloadUtils: exists.attempt = 0 return exists - assert 'foo.bar' == get_unique_filename('foo.bar', attempts(0)) - assert 'foo.bar-1' == get_unique_filename('foo.bar', attempts(1)) - assert 'foo.bar-10' == get_unique_filename('foo.bar', attempts(10)) + get_filename_max_length.return_value = 10 + + actual = get_unique_filename(orig_name, attempts(unique_on_attempt)) + assert expected == actual class TestDownloads: