diff --git a/src/ocrmypdf/_pipeline.py b/src/ocrmypdf/_pipeline.py index 56c17e35..445d0faa 100644 --- a/src/ocrmypdf/_pipeline.py +++ b/src/ocrmypdf/_pipeline.py @@ -37,7 +37,7 @@ from .exceptions import ( UnsupportedImageFormatError, ) from .exec import ghostscript, tesseract -from .helpers import re_symlink +from .helpers import safe_symlink from .hocrtransform import HocrTransform from .optimize import optimize from .pdfa import generate_pdfa_ps @@ -132,7 +132,7 @@ def triage(input_file, output_file, options, log): "input file is a PDF, not an image." ) # Origin file is a pdf create a symlink with pdf extension - re_symlink(input_file, output_file) + safe_symlink(input_file, output_file) return output_file except EnvironmentError as e: log.error(e) @@ -701,7 +701,7 @@ def convert_to_pdfa(input_pdf, input_ps_stub, context): if modified: pdf_file.save(fix_docinfo_file) else: - os.symlink(input_pdf, fix_docinfo_file) + safe_symlink(input_pdf, fix_docinfo_file) ghostscript.generate_pdfa( pdf_version=input_pdfinfo.min_version, diff --git a/src/ocrmypdf/_validation.py b/src/ocrmypdf/_validation.py index b9df4b12..b02e4532 100644 --- a/src/ocrmypdf/_validation.py +++ b/src/ocrmypdf/_validation.py @@ -42,7 +42,7 @@ from .exec import ( tesseract, unpaper, ) -from .helpers import is_file_writable, is_iterable_notstr, monotonic, re_symlink +from .helpers import is_file_writable, is_iterable_notstr, monotonic, safe_symlink # ------------- # External dependencies @@ -374,7 +374,7 @@ def create_input_file(options, work_folder): else: try: target = os.path.join(work_folder, 'origin') - re_symlink(options.input_file, target) + safe_symlink(options.input_file, target) return target except FileNotFoundError: raise InputFileError(f"File not found - {options.input_file}") diff --git a/src/ocrmypdf/exec/tesseract.py b/src/ocrmypdf/exec/tesseract.py index afa0e4a6..cb837dfd 100644 --- a/src/ocrmypdf/exec/tesseract.py +++ b/src/ocrmypdf/exec/tesseract.py @@ -30,7 +30,7 @@ from ..exceptions import ( SubprocessOutputError, TesseractConfigError, ) -from ..helpers import page_number +from ..helpers import page_number, safe_symlink from . import get_version OrientationConfidence = namedtuple('OrientationConfidence', ('angle', 'confidence')) @@ -324,7 +324,7 @@ def use_skip_page(text_only, skip_pdf, output_pdf, output_text): # Substitute a "skipped page" with suppress(FileNotFoundError): os.remove(output_pdf) # In case it was partially created - os.symlink(skip_pdf, output_pdf) + safe_symlink(skip_pdf, output_pdf) return # Or normally, just write a 0 byte file to the output to indicate a skip diff --git a/src/ocrmypdf/helpers.py b/src/ocrmypdf/helpers.py index 80eff55f..b719d4e7 100644 --- a/src/ocrmypdf/helpers.py +++ b/src/ocrmypdf/helpers.py @@ -18,6 +18,7 @@ import logging import multiprocessing import os +import shutil import warnings from collections.abc import Iterable from contextlib import suppress @@ -27,14 +28,14 @@ from pathlib import Path log = logging.getLogger(__name__) -def re_symlink(input_file, soft_link_name, *args, **kwargs): +def safe_symlink(input_file, soft_link_name, *args, **kwargs): """ Helper function: relinks soft symbolic link if necessary """ if len(args) == 1 and isinstance(args[0], logging.Logger): - log.warning("Deprecated: re_symlink(,log)") + log.warning("Deprecated: safe_symlink(,log)") if 'log' in kwargs: - log.warning('Deprecated: re_symlink(...log=)') + log.warning('Deprecated: safe_symlink(...log=)') input_file = os.fspath(input_file) soft_link_name = os.fspath(soft_link_name) @@ -60,6 +61,11 @@ def re_symlink(input_file, soft_link_name, *args, **kwargs): if not os.path.exists(input_file): raise FileNotFoundError(f"trying to create a broken symlink to {input_file}") + if os.name == 'nt': + # Don't actually use symlinks on Windows due to permission issues + shutil.copyfile(input_file, soft_link_name) + return + log.debug("os.symlink(%s, %s)", input_file, soft_link_name) # Create symbolic link using absolute path diff --git a/src/ocrmypdf/optimize.py b/src/ocrmypdf/optimize.py index bb1269db..72022741 100644 --- a/src/ocrmypdf/optimize.py +++ b/src/ocrmypdf/optimize.py @@ -31,7 +31,7 @@ from . import leptonica from ._jobcontext import PDFContext from .exceptions import OutputFileAccessError from .exec import jbig2enc, pngquant -from .helpers import re_symlink +from .helpers import safe_symlink DEFAULT_JPEG_QUALITY = 75 DEFAULT_PNG_QUALITY = 70 @@ -492,7 +492,7 @@ def optimize(input_file, output_file, context, save_settings): log = context.log options = context.options if options.optimize == 0: - re_symlink(input_file, output_file) + safe_symlink(input_file, output_file) return if options.jpeg_quality == 0: @@ -538,7 +538,7 @@ def optimize(input_file, output_file, context, save_settings): pike.remove_unreferenced_resources() pike.save(output_file, **save_settings) else: - re_symlink(target_file, output_file) + safe_symlink(target_file, output_file) def main(infile, outfile, level, jobs=1): diff --git a/tests/test_main.py b/tests/test_main.py index 79625d12..c55186b0 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -946,6 +946,7 @@ def test_output_is_dir(spoof_tesseract_noop, resources, outdir): assert 'is not a writable file' in err +@pytest.mark.skipif(os.name == 'nt', reason="symlink needs admin permissions") def test_output_is_symlink(spoof_tesseract_noop, resources, outdir): sym = Path(outdir / 'this_is_a_symlink') sym.symlink_to(outdir / 'out.pdf')