From 9a15a4db10555756880b8e4d5f1de04424130185 Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Thu, 26 Jan 2017 22:08:24 -0800 Subject: [PATCH] Ensure specified destination is writable before starting pipeline process --- ocrmypdf/__main__.py | 6 +++++- ocrmypdf/helpers.py | 24 ++++++++++++++++++++++++ tests/test_main.py | 10 ++++++++++ tests/test_tess4.py | 9 +-------- 4 files changed, 40 insertions(+), 9 deletions(-) diff --git a/ocrmypdf/__main__.py b/ocrmypdf/__main__.py index 3791a019..838e9820 100755 --- a/ocrmypdf/__main__.py +++ b/ocrmypdf/__main__.py @@ -26,7 +26,7 @@ import ruffus.proxy_logger as proxy_logger from .pipeline import JobContext, JobContextManager, re_symlink, \ cleanup_working_files, build_pipeline from .pdfa import file_claims_pdfa -from .helpers import is_iterable_notstr, re_symlink +from .helpers import is_iterable_notstr, re_symlink, is_file_writable from .exec import tesseract, qpdf from . import PROGRAM_NAME, VERSION @@ -518,6 +518,10 @@ def run_pipeline(): is connected to a terminal. Please redirect stdout to a file.""")) return ExitCode.bad_args + elif not is_file_writable(options.output_file): + _log.error(textwrap.dedent("""\ + Cutput file location is not writable.""")) + return ExitCode.file_access_error manager = JobContextManager() manager.register('JobContext', JobContext) diff --git a/ocrmypdf/helpers.py b/ocrmypdf/helpers.py index 8da245db..655debba 100644 --- a/ocrmypdf/helpers.py +++ b/ocrmypdf/helpers.py @@ -3,6 +3,7 @@ from functools import partial from collections.abc import Iterable +from contextlib import suppress import sys import os @@ -53,3 +54,26 @@ def is_iterable_notstr(thing): def page_number(input_file): return int(os.path.basename(input_file)[0:6]) + + +def is_file_writable(test_file): + """Intentionally racy test if target is writable. + + We intend to write to the output file if and only if we succeed and + can replace it atomically. Before doing the OCR work, make sure + the location is writable. + """ + if os.path.exists(test_file): + return os.access( + test_file, os.W_OK, + effective_ids=(os.access in os.supports_effective_ids)) + else: + try: + fp = open(test_file, 'wb') + except OSError as e: + return False + else: + fp.close() + with suppress(OSError): + os.unlink(test_file) + return True diff --git a/tests/test_main.py b/tests/test_main.py index 7a736d82..1ce5cc09 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -680,3 +680,13 @@ def test_very_high_dpi(spoof_tesseract_cache, resources, outpdf): def test_overlay(spoof_tesseract_noop, resources, outpdf): check_ocrmypdf(resources / 'overlay.pdf', outpdf, env=spoof_tesseract_noop) + + +def test_destination_not_writable(spoof_tesseract_noop, resources, outdir): + protected_file = outdir / 'protected.pdf' + protected_file.touch() + protected_file.chmod(0o400) # Read-only + p, out, err = run_ocrmypdf( + resources / 'jbig2.pdf', protected_file, + env=spoof_tesseract_noop) + assert p.returncode == ExitCode.file_access_error, "Expected error" diff --git a/tests/test_tess4.py b/tests/test_tess4.py index ba97d263..efbbc8e6 100644 --- a/tests/test_tess4.py +++ b/tests/test_tess4.py @@ -15,20 +15,13 @@ from ocrmypdf.pdfa import file_claims_pdfa from ocrmypdf.exec import tesseract -TESTS_ROOT = os.path.abspath(os.path.dirname(__file__)) -SPOOF_PATH = os.path.join(TESTS_ROOT, 'spoof') -PROJECT_ROOT = os.path.dirname(TESTS_ROOT) -TEST_RESOURCES = os.path.join(PROJECT_ROOT, 'tests', 'resources') -OCRMYPDF = [sys.executable, '-m', 'ocrmypdf'] - - # Skip all tests in this file if not tesseract 4 pytestmark = pytest.mark.skipif(not tesseract.v4(), reason="tesseract 4.0 required") @pytest.mark.skipif(not tesseract.has_textonly_pdf(), - reason="requires textonly_pdf parameter") + reason="requires textonly_pdf feature") def test_textonly_pdf(resources, outdir): pytest.helpers.check_ocrmypdf( resources / 'linn.pdf',