import osimport randomimport stringimport timefrom typing import Iterable, UnionIS_WINDOWS = os.name == 'nt'# Max attempt if another process is reading/writing, effective only on WindowsWINDOWS_MAX_ATTEMPT = 5# Base time to wait between retries (seconds)WINDOWS_RETRY_DELAY = 0.05def random_id():"""Returns:str: Random ID, like "sTD2kF""""# 6 random letter (62^6 combinations) would be enoughreturn ''.join(random.sample(string.ascii_letters + string.digits, 6))def is_tmp_file(file: str) -> bool:"""Check if a filename is tmp file"""# Check suffix first to reduce regex callsif not file.endswith('.tmp'):return False# Check temp file formatdot = file[-11:-10]if not dot:return Falserid = file[-10:-4]return rid.isalnum()def to_tmp_file(file: str) -> str:"""Convert a filename or directory name to tmpfilename -> filename.sTD2kF.tmp"""suffix = random_id()return f'{file}.{suffix}.tmp'def to_nontmp_file(file: str) -> str:"""Convert a tmp filename or directory name to original filefilename.sTD2kF.tmp -> filename"""if is_tmp_file(file):return file[:-11]else:return filedef windows_attempt_delay(attempt: int) -> float:"""Exponential Backoff if file is in use on WindowsArgs:attempt: Current attempt, starting from 0Returns:float: Seconds to wait"""return 2 ** attempt * WINDOWS_RETRY_DELAYdef replace_tmp(tmp: str, file: str):"""Replace temp file to fileRaises:PermissionError: (Windows only) If another process is still reading the file and all retries failedFileNotFoundError: If tmp file gets deleted unexpectedly"""if IS_WINDOWS:# PermissionError on Windows if another process is readinglast_error = Nonefor attempt in range(WINDOWS_MAX_ATTEMPT):try:# Atomic operationos.replace(tmp, file)# successreturnexcept PermissionError as e:last_error = edelay = windows_attempt_delay(attempt)time.sleep(delay)continueexcept FileNotFoundError:# tmp file gets deleted unexpectedlyraiseexcept Exception as e:last_error = ebreakelse:# Linux and Mac allow existing readingtry:# Atomic operationos.replace(tmp, file)# successreturnexcept FileNotFoundError:raiseexcept Exception as e:last_error = e# Clean up tmp file on failuretry:os.unlink(tmp)except FileNotFoundError:# tmp file already get deletedpassexcept:passif last_error is not None:raise last_error from Nonedef atomic_replace(replace_from: str, replace_to: str):"""Replace file or directoryRaises:PermissionError: (Windows only) If another process is still reading the file and all retries failedFileNotFoundError:"""if IS_WINDOWS:# PermissionError on Windows if another process is readinglast_error = Nonefor attempt in range(WINDOWS_MAX_ATTEMPT):try:# Atomic operationos.replace(replace_from, replace_to)# successreturnexcept PermissionError as e:last_error = edelay = windows_attempt_delay(attempt)time.sleep(delay)continueexcept FileNotFoundError:raiseexcept Exception as e:last_error = ebreakif last_error is not None:raise last_error from Noneelse:# Linux and Macos.replace(replace_from, replace_to)def file_write(file: str, data: Union[str, bytes]):"""Write data into file, auto create directoryAuto determines write mode based on the type of data."""if isinstance(data, str):mode = 'w'encoding = 'utf-8'newline = ''elif isinstance(data, bytes):mode = 'wb'encoding = Nonenewline = None# Create memoryview as Pathlib dodata = memoryview(data)else:typename = str(type(data))if typename == "<class 'numpy.ndarray'>":mode = 'wb'encoding = Nonenewline = Noneelse:mode = 'w'encoding = 'utf-8'newline = ''try:# Write temp filewith open(file, mode=mode, encoding=encoding, newline=newline) as f:f.write(data)# Ensure data flush to diskf.flush()os.fsync(f.fileno())except FileNotFoundError:# Create parent directorydirectory = os.path.dirname(file)if directory:os.makedirs(directory, exist_ok=True)# Write againwith open(file, mode=mode, encoding=encoding, newline=newline) as f:f.write(data)# Ensure data flush to diskf.flush()os.fsync(f.fileno())def file_write_stream(file: str, data_generator):"""Only creates a file if the generator yields at least one data chunk.Auto determines write mode based on the type of first chunk.Args:file: Target file pathdata_generator: An iterable that yields data chunks (str or bytes)"""# Convert generator to iterator to ensure we can peek at first chunkdata_iter = iter(data_generator)# Try to get the first chunktry:first_chunk = next(data_iter)except StopIteration:# Generator is empty, no file will be createdreturn# Determine mode, encoding and newline from first chunkif isinstance(first_chunk, str):mode = 'w'encoding = 'utf-8'newline = ''elif isinstance(first_chunk, bytes):mode = 'wb'encoding = Nonenewline = Noneelse:# Default to text mode for other typesmode = 'w'encoding = 'utf-8'newline = ''try:# Write temp filewith open(file, mode=mode, encoding=encoding, newline=newline) as f:f.write(first_chunk)for chunk in data_iter:f.write(chunk)# Ensure data flush to diskf.flush()os.fsync(f.fileno())except FileNotFoundError:# Create parent directorydirectory = os.path.dirname(file)if directory:os.makedirs(directory, exist_ok=True)# Write againwith open(file, mode=mode, encoding=encoding, newline=newline) as f:f.write(first_chunk)for chunk in data_iter:f.write(chunk)# Ensure data flush to diskf.flush()os.fsync(f.fileno())def atomic_write(file: str,data: Union[str, bytes],):"""Atomic file write with minimal IO operationand handles cases where file might be read by another process.os.replace() is an atomic operation among all OS,we write to temp file then do os.replace()Args:file:data:"""temp = to_tmp_file(file)file_write(temp, data)replace_tmp(temp, file)def atomic_write_stream(file: str,data_generator,):"""Atomic file write with streaming data support.Handles cases where file might be read by another process.os.replace() is an atomic operation among all OS,we write to temp file then do os.replace()Args:file: Target file pathdata_generator: An iterable that yields data chunks (str or bytes)"""temp = to_tmp_file(file)file_write_stream(temp, data_generator)replace_tmp(temp, file)def file_read_text(file: str,encoding: str = 'utf-8',errors: str = 'strict') -> str:"""Args:file:encoding:errors: 'strict', 'ignore', 'replace' and any other errors mode in open()"""try:with open(file, mode='r', encoding=encoding, errors=errors) as f:return f.read()except FileNotFoundError:return ''def file_read_text_stream(file: str,encoding: str = 'utf-8',errors: str = 'strict',chunk_size: int = 8192) -> Iterable[str]:"""Args:file:encoding:errors: 'strict', 'ignore', 'replace' and any other errors mode in open()chunk_size:"""try:with open(file, mode='r', encoding=encoding, errors=errors) as f:while 1:chunk = f.read(chunk_size)if not chunk:returnyield chunkexcept FileNotFoundError:returndef file_read_bytes(file: str) -> bytes:"""Args:file:"""try:# No python-side buffering when reading the entire file to speedup reading# https://github.com/python/cpython/pull/122111with open(file, mode='rb', buffering=0) as f:return f.read()except FileNotFoundError:return b''def file_read_bytes_stream(file: str, chunk_size: int = 8192) -> Iterable[bytes]:"""Args:file:chunk_size:"""try:with open(file, mode='rb') as f:while 1:chunk = f.read(chunk_size)if not chunk:returnyield chunkexcept FileNotFoundError:returndef atomic_read_text(file: str,encoding: str = 'utf-8',errors: str = 'strict') -> str:"""Atomic file read with minimal IO operationArgs:file:encoding:errors: 'strict', 'ignore', 'replace' and any other errors mode in open()"""if IS_WINDOWS:# PermissionError on Windows if another process is replacinglast_error = Nonefor attempt in range(WINDOWS_MAX_ATTEMPT):try:return file_read_text(file, encoding=encoding, errors=errors)except PermissionError as e:last_error = edelay = windows_attempt_delay(attempt)time.sleep(delay)continueif last_error is not None:raise last_error from Noneelse:# Linux and Mac allow reading while replacingreturn file_read_text(file, encoding=encoding, errors=errors)def atomic_read_text_stream(file: str,encoding: str = 'utf-8',errors: str = 'strict',chunk_size: int = 8192) -> Iterable[str]:"""Args:file:encoding:errors: 'strict', 'ignore', 'replace' and any other errors mode in open()chunk_size:"""if IS_WINDOWS:# PermissionError on Windows if another process is replacinglast_error = Nonefor attempt in range(WINDOWS_MAX_ATTEMPT):try:yield from file_read_text_stream(file, encoding=encoding, errors=errors, chunk_size=chunk_size)returnexcept PermissionError as e:last_error = edelay = windows_attempt_delay(attempt)time.sleep(delay)continueif last_error is not None:raise last_error from Noneelse:# Linux and Mac allow reading while replacingyield from file_read_text_stream(file, encoding=encoding, errors=errors, chunk_size=chunk_size)returndef atomic_read_bytes(file: str) -> bytes:"""Atomic file read with minimal IO operation"""if IS_WINDOWS:# PermissionError on Windows if another process is replacinglast_error = Nonefor attempt in range(WINDOWS_MAX_ATTEMPT):try:return file_read_bytes(file)except PermissionError as e:last_error = edelay = windows_attempt_delay(attempt)time.sleep(delay)continueif last_error is not None:raise last_error from Noneelse:# Linux and Mac allow reading while replacingreturn file_read_bytes(file)def atomic_read_bytes_stream(file: str, chunk_size: int = 8192) -> Iterable[bytes]:"""Args:file:chunk_size:"""if IS_WINDOWS:# PermissionError on Windows if another process is replacinglast_error = Nonefor attempt in range(WINDOWS_MAX_ATTEMPT):try:yield from file_read_bytes_stream(file, chunk_size=chunk_size)returnexcept PermissionError as e:last_error = edelay = windows_attempt_delay(attempt)time.sleep(delay)continueif last_error is not None:raise last_error from Noneelse:# Linux and Mac allow reading while replacingyield from file_read_bytes_stream(file, chunk_size=chunk_size)returndef file_remove(file: str):"""Remove a file non-atomic"""try:os.unlink(file)except FileNotFoundError:# If file not exist, just no need to removepassdef atomic_remove(file: str):"""Atomic file removeArgs:file:"""if IS_WINDOWS:# PermissionError on Windows if another process is replacinglast_error = Nonefor attempt in range(WINDOWS_MAX_ATTEMPT):try:return file_remove(file)except PermissionError as e:last_error = edelay = windows_attempt_delay(attempt)time.sleep(delay)continueif last_error is not None:raise last_error from Noneelse:# Linux and Mac allow deleting while another process is reading# The directory entry is removed but the storage allocated to the file is not made available# until the original file is no longer in use.return file_remove(file)def folder_rmtree(folder, may_symlinks=True):"""Recursively remove a folder and its contentArgs:folder:may_symlinks: Default to TrueFalse if you already know it's not a symlinkReturns:bool: If success"""try:# If it's a symlinks, unlink itif may_symlinks and os.path.islink(folder):file_remove(folder)return True# Iter folderwith os.scandir(folder) as entries:for entry in entries:if entry.is_dir(follow_symlinks=False):folder_rmtree(entry.path, may_symlinks=False)else:# File or symlink# Just remove the symlink, not what it points totry:file_remove(entry.path)except PermissionError:# Another process is reading/writingpassexcept FileNotFoundError:# directory to clean up does not exist, no need to clean upreturn Trueexcept NotADirectoryError:file_remove(folder)return True# Remove empty folder# May raise OSError if it's still not emptytry:os.rmdir(folder)return Trueexcept FileNotFoundError:return Trueexcept NotADirectoryError:file_remove(folder)return Trueexcept OSError:return Falsedef atomic_rmtree(folder: str):"""Atomic folder rmtreeRename folder as temp folder and remove it,folder can be removed by atomic_failure_cleanup at next startup if remove gets interrupted"""temp = to_tmp_file(folder)try:atomic_replace(folder, temp)except FileNotFoundError:# Folder not exist, no need to rmtreereturnfolder_rmtree(temp)def atomic_failure_cleanup(folder: str, recursive: bool = False):"""Cleanup remaining temp file under given path.In most cases there should be no remaining temp files unless write process get interrupted.This method should only be called at startupto avoid deleting temp files that another process is writing."""try:with os.scandir(folder) as entries:for entry in entries:if is_tmp_file(entry.name):try:# Delete temp file or directoryif entry.is_dir(follow_symlinks=False):folder_rmtree(entry.path, may_symlinks=False)else:file_remove(entry.path)except PermissionError:# Another process is reading/writingpassexcept:passelse:if recursive:try:if entry.is_dir(follow_symlinks=False):# Normal directoryatomic_failure_cleanup(entry.path, recursive=True)except:passexcept FileNotFoundError:# directory to clean up does not exist, no need to clean uppassexcept NotADirectoryError:file_remove(folder)except:# Ignore all failures, it doesn't matter if tmp files still existpass
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。