diff --git a/yt_dlp/downloader/common.py b/yt_dlp/downloader/common.py index afd2f2e38..cbfea7a65 100644 --- a/yt_dlp/downloader/common.py +++ b/yt_dlp/downloader/common.py @@ -11,6 +11,7 @@ from ..utils import ( encodeFilename, error_to_compat_str, format_bytes, + LockingUnsupportedError, sanitize_open, shell_quote, timeconvert, @@ -234,7 +235,10 @@ class FileDownloader(object): @wrap_file_access('open', fatal=True) def sanitize_open(self, filename, open_mode): - return sanitize_open(filename, open_mode) + f, filename = sanitize_open(filename, open_mode) + if not getattr(f, 'locked', None): + self.write_debug(f'{LockingUnsupportedError.msg}. Proceeding without locking', only_once=True) + return f, filename @wrap_file_access('remove') def try_remove(self, filename): diff --git a/yt_dlp/utils.py b/yt_dlp/utils.py index 6663583fc..2db22d676 100644 --- a/yt_dlp/utils.py +++ b/yt_dlp/utils.py @@ -674,26 +674,25 @@ def sanitize_open(filename, open_mode): It returns the tuple (stream, definitive_file_name). """ - try: - if filename == '-': - if sys.platform == 'win32': - import msvcrt - msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY) - return (sys.stdout.buffer if hasattr(sys.stdout, 'buffer') else sys.stdout, filename) - stream = locked_file(filename, open_mode, block=False).open() - return (stream, filename) - except (IOError, OSError) as err: - if err.errno in (errno.EACCES,): - raise + if filename == '-': + if sys.platform == 'win32': + import msvcrt + msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY) + return (sys.stdout.buffer if hasattr(sys.stdout, 'buffer') else sys.stdout, filename) - # In case of error, try to remove win32 forbidden chars - alt_filename = sanitize_path(filename) - if alt_filename == filename: - raise - else: - # An exception here should be caught in the caller - stream = locked_file(filename, open_mode, block=False).open() - return (stream, alt_filename) + for attempt in range(2): + try: + try: + stream = locked_file(filename, open_mode, block=False).__enter__() + except LockingUnsupportedError: + stream = open(filename, open_mode) + return (stream, filename) + except (IOError, OSError) as err: + if attempt or err.errno in (errno.EACCES,): + raise + old_filename, filename = filename, sanitize_path(filename) + if old_filename == filename: + raise def timeconvert(timestr): @@ -2120,6 +2119,13 @@ def intlist_to_bytes(xs): return compat_struct_pack('%dB' % len(xs), *xs) +class LockingUnsupportedError(IOError): + msg = 'File locking is not supported on this platform' + + def __init__(self): + super().__init__(self.msg) + + # Cross-platform file locking if sys.platform == 'win32': import ctypes.wintypes @@ -2200,21 +2206,20 @@ else: fcntl.lockf(f, fcntl.LOCK_UN) except ImportError: - UNSUPPORTED_MSG = 'file locking is not supported on this platform' def _lock_file(f, exclusive, block): - raise IOError(UNSUPPORTED_MSG) + raise LockingUnsupportedError() def _unlock_file(f): - raise IOError(UNSUPPORTED_MSG) + raise LockingUnsupportedError() class locked_file(object): - _closed = False + locked = False def __init__(self, filename, mode, block=True, encoding=None): - assert mode in ['r', 'rb', 'a', 'ab', 'w', 'wb'] - self.f = io.open(filename, mode, encoding=encoding) + assert mode in {'r', 'rb', 'a', 'ab', 'w', 'wb'} + self.f = open(filename, mode, encoding=encoding) self.mode = mode self.block = block @@ -2222,37 +2227,35 @@ class locked_file(object): exclusive = 'r' not in self.mode try: _lock_file(self.f, exclusive, self.block) + self.locked = True except IOError: self.f.close() raise return self - def __exit__(self, etype, value, traceback): + def unlock(self): + if not self.locked: + return try: - if not self._closed: - _unlock_file(self.f) + _unlock_file(self.f) + finally: + self.locked = False + + def __exit__(self, *_): + try: + self.unlock() finally: self.f.close() - self._closed = True + + open = __enter__ + close = __exit__ + + def __getattr__(self, attr): + return getattr(self.f, attr) def __iter__(self): return iter(self.f) - def write(self, *args): - return self.f.write(*args) - - def read(self, *args): - return self.f.read(*args) - - def flush(self): - self.f.flush() - - def open(self): - return self.__enter__() - - def close(self, *args): - self.__exit__(self, *args, value=False, traceback=False) - def get_filesystem_encoding(): encoding = sys.getfilesystemencoding()