Skip to content

Commit

Permalink
fix: add support for long filenames on export
Browse files Browse the repository at this point in the history
* Fix for file names that are too long for file system
* Improved fix for long file names
  • Loading branch information
RhetTbull committed Dec 31, 2021
1 parent 071ee2b commit 8bea1e6
Show file tree
Hide file tree
Showing 2 changed files with 113 additions and 0 deletions.
33 changes: 33 additions & 0 deletions evernote_backup/note_exporter_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
from typing import Dict


MAX_FILE_NAME_LEN = 255


class SafePath(object):
"""Ensures path for tuples of directory names that may contain bad symbols
Expand Down Expand Up @@ -65,9 +68,39 @@ def _replace_bad_characters(string: str) -> str:


def _get_non_existant_name(safe_name: str, target_dir: str) -> str:
safe_name = _trim_name(safe_name)
i = 0
o_name, o_ext = os.path.splitext(safe_name)
while os.path.exists(os.path.join(target_dir, safe_name)):
i += 1
safe_name = f"{o_name} ({i}){o_ext}"
if len(safe_name) > MAX_FILE_NAME_LEN:
max_len = MAX_FILE_NAME_LEN - len(f" ({i}){o_ext}")
o_name = _trim_name(o_name, max_len)
safe_name = f"{o_name} ({i}){o_ext}"

return safe_name


def _trim_name(safe_name: str, max_len=MAX_FILE_NAME_LEN) -> str:
""" Trim file name to 255 characters while maintaining extension
255 characters is max file name length on linux and macOS
Windows has a path limit of 260 characters which includes
the entire path (drive letter, path, and file name)
This does not trim the path length, just the file name
max_len: if provided, trims to this length otherwise MAX_FILE_NAME_LEN
Raises: ValueError if the file name is too long and cannot be trimmed
"""
if len(safe_name) <= max_len:
return safe_name

drop_chars = len(safe_name) - max_len
file_parts = safe_name.rsplit(".", 1)
if len(file_parts) != 2:
return f"{file_parts[0][:-drop_chars]}"
if len(file_parts[0]) > drop_chars:
return f"{file_parts[0][:-drop_chars]}.{file_parts[1]}"
else:
raise ValueError("File name is too long but cannot be safely trimmed: {safe_name}") # noqa: E501
80 changes: 80 additions & 0 deletions tests/test_note_exporter_util.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import os

import pytest

from evernote_backup import note_exporter_util
from evernote_backup.note_exporter_util import (
SafePath,
Expand Down Expand Up @@ -58,6 +60,64 @@ def test_get_safe_path():
assert expected_result == result


def test_safe_path_long_file_name(tmp_path):
"""Test that SafePath trims a long file name with extension"""
test_dir = tmp_path / "test"
long_file_name = "X" * 255 + ".ext"
expected_file_name = "X" * 251 + ".ext"
expected_file = tmp_path / "test" / "test1" / expected_file_name

safe_path = SafePath(str(test_dir))
result_file_path = safe_path.get_file("test1", long_file_name)

expected_file.touch()

assert expected_file.is_file()
assert result_file_path == str(expected_file)


def test_safe_path_long_file_name_no_ext(tmp_path):
"""Test that SafePath trims a long file name with no extension"""
test_dir = tmp_path / "test"
long_file_name = "X" * 260
expected_file_name = "X" * 255
expected_file = tmp_path / "test" / "test1" / expected_file_name

safe_path = SafePath(str(test_dir))
result_file_path = safe_path.get_file("test1", long_file_name)

expected_file.touch()

assert expected_file.is_file()
assert result_file_path == str(expected_file)


def test_safe_path_long_file_name_invalid(tmp_path):
"""Test that SafePath raises ValueError if path is too long but cannot be trimmed"""
test_dir = tmp_path / "test"
bad_file_name = "X" + "." + "x" * 255

safe_path = SafePath(str(test_dir))
with pytest.raises(ValueError):
safe_path.get_file("test1", bad_file_name)


def test_safe_path_no_trim(tmp_path):
"""Test that the SafePath does not trim the path if the path is not too long"""
test_dir = tmp_path / "test"
max_file_name = "X" * 251 + ".ext"
expected_file_name = max_file_name
expected_file = tmp_path / "test" / "test1" / expected_file_name

safe_path = SafePath(str(test_dir))
result_file_path = safe_path.get_file("test1", max_file_name)

expected_file.touch()

assert expected_file.is_file()
assert result_file_path == str(expected_file)


def test_get_non_existant_name_first(mocker):
mock_file_check = mocker.patch("evernote_backup.note_exporter_util.os.path.exists")
mock_file_check.return_value = False
Expand All @@ -80,6 +140,26 @@ def test_get_non_existant_name(mocker):
assert expected_filename == result_filename


def test_get_non_existant_name_trim(mocker):
"""Test _get_non_existant_name() trims the file name if it is too long"""
initial_name = "X" * 255 + ".ext"
expected_filename = "X" * 251 + ".ext"
result_filename = _get_non_existant_name(initial_name, "fake_dir")

assert expected_filename == result_filename


def test_get_non_existant_name_trim_bad_name(mocker):
"""Test _get_non_existant_name() trims the file name if it is too long after incrementing"""
mock_file_check = mocker.patch("evernote_backup.note_exporter_util.os.path.exists")
mock_file_check.side_effect = [True, True, False]
initial_name = "X" * 251 + ".ext"
expected_filename = "X" * 247 + " (2).ext"
result_filename = _get_non_existant_name(initial_name, "fake_dir")

assert expected_filename == result_filename


def test_replace_bad_characters():
initial_name = r'test<>:"/\|?*'
expected_filename = r"test_________"
Expand Down

0 comments on commit 8bea1e6

Please sign in to comment.