2022-03-23 20:57:10 +00:00
import collections
2015-11-04 22:30:28 +00:00
import os
2022-07-27 21:18:33 +00:00
import typing
2016-04-14 01:54:29 +00:00
import psutil
2018-09-26 19:05:12 +00:00
import re
2015-11-25 22:00:57 +00:00
import send2trash
2017-06-07 22:05:15 +00:00
import shlex
2015-11-25 22:00:57 +00:00
import shutil
2016-05-04 21:50:55 +00:00
import stat
2015-11-04 22:30:28 +00:00
import subprocess
import threading
2015-11-25 22:00:57 +00:00
import traceback
2015-11-04 22:30:28 +00:00
2020-07-29 20:52:44 +00:00
from hydrus . core import HydrusConstants as HC
from hydrus . core import HydrusData
from hydrus . core import HydrusGlobals as HG
from hydrus . core import HydrusThreading
2022-03-23 20:57:10 +00:00
mimes_to_default_thumbnail_paths = collections . defaultdict ( lambda : os . path . join ( HC . STATIC_DIR , ' hydrus.png ' ) )
mimes_to_default_thumbnail_paths [ HC . APPLICATION_PDF ] = os . path . join ( HC . STATIC_DIR , ' pdf.png ' )
mimes_to_default_thumbnail_paths [ HC . APPLICATION_PSD ] = os . path . join ( HC . STATIC_DIR , ' psd.png ' )
mimes_to_default_thumbnail_paths [ HC . APPLICATION_CLIP ] = os . path . join ( HC . STATIC_DIR , ' clip.png ' )
for mime in HC . AUDIO :
path = os . path . join ( HC . STATIC_DIR , ' audio.png ' )
mimes_to_default_thumbnail_paths [ mime ] = os . path . join ( path )
for mime in HC . VIDEO :
path = os . path . join ( HC . STATIC_DIR , ' video.png ' )
mimes_to_default_thumbnail_paths [ mime ] = os . path . join ( path )
for mime in HC . ARCHIVES :
path = os . path . join ( HC . STATIC_DIR , ' zip.png ' )
mimes_to_default_thumbnail_paths [ mime ] = os . path . join ( path )
2016-04-06 19:52:45 +00:00
def AppendPathUntilNoConflicts ( path ) :
( path_absent_ext , ext ) = os . path . splitext ( path )
good_path_absent_ext = path_absent_ext
i = 0
while os . path . exists ( good_path_absent_ext + ext ) :
good_path_absent_ext = path_absent_ext + ' _ ' + str ( i )
i + = 1
return good_path_absent_ext + ext
2016-10-26 20:45:34 +00:00
def ConvertAbsPathToPortablePath ( abs_path , base_dir_override = None ) :
2015-11-04 22:30:28 +00:00
2016-05-04 21:50:55 +00:00
try :
2016-10-26 20:45:34 +00:00
if base_dir_override is None :
2017-05-10 21:33:58 +00:00
base_dir = HG . controller . GetDBDir ( )
2016-10-26 20:45:34 +00:00
else :
base_dir = base_dir_override
portable_path = os . path . relpath ( abs_path , base_dir )
if portable_path . startswith ( ' .. ' ) :
portable_path = abs_path
2016-05-04 21:50:55 +00:00
except :
portable_path = abs_path
if HC . PLATFORM_WINDOWS :
portable_path = portable_path . replace ( ' \\ ' , ' / ' ) # store seps as /, to maintain multiplatform uniformity
return portable_path
2015-11-04 22:30:28 +00:00
2016-10-26 20:45:34 +00:00
def ConvertPortablePathToAbsPath ( portable_path , base_dir_override = None ) :
2015-11-25 22:00:57 +00:00
2016-05-04 21:50:55 +00:00
portable_path = os . path . normpath ( portable_path ) # collapses .. stuff and converts / to \\ for windows only
if os . path . isabs ( portable_path ) :
abs_path = portable_path
else :
2016-10-26 20:45:34 +00:00
if base_dir_override is None :
2017-05-10 21:33:58 +00:00
base_dir = HG . controller . GetDBDir ( )
2016-10-26 20:45:34 +00:00
else :
base_dir = base_dir_override
abs_path = os . path . normpath ( os . path . join ( base_dir , portable_path ) )
2016-05-04 21:50:55 +00:00
if not HC . PLATFORM_WINDOWS and not os . path . exists ( abs_path ) :
abs_path = abs_path . replace ( ' \\ ' , ' / ' )
2015-11-25 22:00:57 +00:00
2015-12-02 22:32:18 +00:00
return abs_path
def CopyAndMergeTree ( source , dest ) :
2016-01-06 21:17:20 +00:00
pauser = HydrusData . BigJobPauser ( )
2016-08-17 20:07:22 +00:00
MakeSureDirectoryExists ( dest )
2015-12-02 22:32:18 +00:00
2016-06-01 20:04:15 +00:00
num_errors = 0
2015-12-02 22:32:18 +00:00
for ( root , dirnames , filenames ) in os . walk ( source ) :
dest_root = root . replace ( source , dest )
for dirname in dirnames :
2016-01-06 21:17:20 +00:00
pauser . Pause ( )
2015-12-02 22:32:18 +00:00
source_path = os . path . join ( root , dirname )
dest_path = os . path . join ( dest_root , dirname )
2016-08-17 20:07:22 +00:00
MakeSureDirectoryExists ( dest_path )
2015-12-02 22:32:18 +00:00
shutil . copystat ( source_path , dest_path )
for filename in filenames :
2016-06-01 20:04:15 +00:00
if num_errors > 5 :
raise Exception ( ' Too many errors, directory copy abandoned. ' )
2016-01-06 21:17:20 +00:00
pauser . Pause ( )
2015-12-02 22:32:18 +00:00
source_path = os . path . join ( root , filename )
dest_path = os . path . join ( dest_root , filename )
2016-06-01 20:04:15 +00:00
ok = MirrorFile ( source_path , dest_path )
if not ok :
2016-05-18 20:07:14 +00:00
2016-06-01 20:04:15 +00:00
num_errors + = 1
2016-05-18 20:07:14 +00:00
2015-12-02 22:32:18 +00:00
2015-11-25 22:00:57 +00:00
2015-11-04 22:30:28 +00:00
def CopyFileLikeToFileLike ( f_source , f_dest ) :
for block in ReadFileLikeAsBlocks ( f_source ) : f_dest . write ( block )
2015-11-25 22:00:57 +00:00
def DeletePath ( path ) :
2019-07-24 21:39:02 +00:00
if HG . file_report_mode :
HydrusData . ShowText ( ' Deleting {} ' . format ( path ) )
HydrusData . ShowText ( ' ' . join ( traceback . format_stack ( ) ) )
2015-11-25 22:00:57 +00:00
if os . path . exists ( path ) :
2022-01-12 22:14:50 +00:00
TryToMakeFileWriteable ( path )
2016-05-04 21:50:55 +00:00
2016-06-08 20:27:22 +00:00
try :
2015-11-25 22:00:57 +00:00
2016-06-08 20:27:22 +00:00
if os . path . isdir ( path ) :
shutil . rmtree ( path )
else :
os . remove ( path )
2015-11-25 22:00:57 +00:00
2016-06-08 20:27:22 +00:00
except Exception as e :
2015-11-25 22:00:57 +00:00
2019-01-09 22:59:03 +00:00
if ' Error 32 ' in str ( e ) :
2016-10-19 20:02:56 +00:00
# file in use by another process
HydrusData . DebugPrint ( ' Trying to delete ' + path + ' failed because it was in use by another process. ' )
else :
HydrusData . ShowText ( ' Trying to delete ' + path + ' caused the following error: ' )
HydrusData . ShowException ( e )
2015-11-25 22:00:57 +00:00
2016-06-08 20:27:22 +00:00
2021-02-17 18:22:44 +00:00
def DirectoryIsWriteable ( path ) :
2018-02-21 21:59:37 +00:00
2021-02-17 18:22:44 +00:00
while not os . path . exists ( path ) :
2018-05-23 21:05:06 +00:00
2021-02-17 18:22:44 +00:00
try :
path = os . path . dirname ( path )
except :
return False
2022-07-13 21:35:17 +00:00
if not os . access ( path , os . W_OK | os . X_OK ) :
2021-02-17 18:22:44 +00:00
2022-07-13 21:35:17 +00:00
return False
2018-05-23 21:05:06 +00:00
2022-07-13 21:35:17 +00:00
# we'll actually do a file, since Program Files passes the above test lmaoooo
2018-02-21 21:59:37 +00:00
try :
2022-07-13 21:35:17 +00:00
# also, using tempfile.TemporaryFile actually loops on PermissionError from Windows lmaaaooooo, thinking this is an already existing file
2021-02-17 18:22:44 +00:00
# so, just do it manually!
2022-07-13 21:35:17 +00:00
test_path = os . path . join ( path , ' hydrus_permission_test ' )
2018-02-21 21:59:37 +00:00
2022-07-13 21:35:17 +00:00
with open ( test_path , ' wb ' ) as f :
f . write ( b ' If this file still exists, this directory can be written to but not deleted from. ' )
2018-02-21 21:59:37 +00:00
2022-07-13 21:35:17 +00:00
os . unlink ( test_path )
2018-02-21 21:59:37 +00:00
except :
return False
2022-07-13 21:35:17 +00:00
return True
2022-01-05 22:15:56 +00:00
def FileisWriteable ( path : str ) :
2022-01-12 22:14:50 +00:00
return os . access ( path , os . W_OK )
2022-01-05 22:15:56 +00:00
2016-08-24 18:36:56 +00:00
def FilterFreePaths ( paths ) :
free_paths = [ ]
for path in paths :
2019-01-23 22:19:16 +00:00
HydrusThreading . CheckIfThreadShuttingDown ( )
2017-11-08 22:07:12 +00:00
2018-01-24 23:09:42 +00:00
if PathIsFree ( path ) :
2016-08-24 18:36:56 +00:00
free_paths . append ( path )
return free_paths
2017-09-20 19:47:31 +00:00
def GetDefaultLaunchPath ( ) :
if HC . PLATFORM_WINDOWS :
return ' windows is called directly '
2019-11-20 23:10:46 +00:00
elif HC . PLATFORM_MACOS :
2017-09-20 19:47:31 +00:00
return ' open " % path % " '
elif HC . PLATFORM_LINUX :
return ' xdg-open " % path % " '
2020-07-15 20:52:09 +00:00
elif HC . PLATFORM_HAIKU :
return ' open " % path % " '
2017-09-20 19:47:31 +00:00
2022-07-27 21:18:33 +00:00
def GetPartitionInfo ( path ) - > typing . Optional [ typing . NamedTuple ] :
2016-04-14 01:54:29 +00:00
path = path . lower ( )
2019-07-31 22:01:02 +00:00
try :
2016-04-14 01:54:29 +00:00
2020-08-19 22:38:20 +00:00
for scan_network in ( False , True ) :
2019-07-31 22:01:02 +00:00
2020-08-19 22:38:20 +00:00
partition_infos = psutil . disk_partitions ( all = scan_network )
2019-07-31 22:01:02 +00:00
2020-08-19 22:38:20 +00:00
def sort_descending_mountpoint ( partition_info ) : # i.e. put '/home' before '/'
return - len ( partition_info . mountpoint )
partition_infos . sort ( key = sort_descending_mountpoint )
2016-04-14 01:54:29 +00:00
2020-08-19 22:38:20 +00:00
for partition_info in partition_infos :
2019-07-31 22:01:02 +00:00
2020-08-19 22:38:20 +00:00
if path . startswith ( partition_info . mountpoint . lower ( ) ) :
2022-07-27 21:18:33 +00:00
return partition_info
2020-08-19 22:38:20 +00:00
2019-07-31 22:01:02 +00:00
2016-04-14 01:54:29 +00:00
2019-07-31 22:01:02 +00:00
except UnicodeDecodeError : # wew lad psutil on some russian lad's fun filesystem
return None
2016-04-14 01:54:29 +00:00
return None
2016-01-06 21:17:20 +00:00
2022-07-27 21:18:33 +00:00
def GetDevice ( path ) - > typing . Optional [ str ] :
partition_info = GetPartitionInfo ( path )
if partition_info is None :
return None
else :
return partition_info . device
def GetFileSystemType ( path ) :
partition_info = GetPartitionInfo ( path )
if partition_info is None :
return None
else :
return partition_info . fstype
2017-02-08 22:27:00 +00:00
def GetFreeSpace ( path ) :
disk_usage = psutil . disk_usage ( path )
return disk_usage . free
2022-07-13 21:35:17 +00:00
def GetTotalSpace ( path ) :
disk_usage = psutil . disk_usage ( path )
return disk_usage . total
2015-11-04 22:30:28 +00:00
def LaunchDirectory ( path ) :
def do_it ( ) :
if HC . PLATFORM_WINDOWS :
os . startfile ( path )
else :
2019-11-20 23:10:46 +00:00
if HC . PLATFORM_MACOS :
2017-06-07 22:05:15 +00:00
2019-01-09 22:59:03 +00:00
cmd = [ ' open ' , path ]
2017-06-07 22:05:15 +00:00
elif HC . PLATFORM_LINUX :
2019-01-09 22:59:03 +00:00
cmd = [ ' xdg-open ' , path ]
2017-06-07 22:05:15 +00:00
2020-07-15 20:52:09 +00:00
elif HC . PLATFORM_HAIKU :
cmd = [ ' open ' , path ]
2015-11-04 22:30:28 +00:00
2017-04-12 21:46:46 +00:00
# setsid call un-childs this new process
2019-01-09 22:59:03 +00:00
sbp_kwargs = HydrusData . GetSubprocessKWArgs ( )
2020-03-25 21:15:57 +00:00
preexec_fn = getattr ( os , ' setsid ' , None )
2020-06-17 21:31:54 +00:00
HydrusData . CheckProgramIsNotShuttingDown ( )
2020-03-25 21:15:57 +00:00
process = subprocess . Popen ( cmd , preexec_fn = preexec_fn , * * sbp_kwargs )
2015-11-04 22:30:28 +00:00
2020-06-17 21:31:54 +00:00
HydrusThreading . SubprocessCommunicate ( process )
2015-11-04 22:30:28 +00:00
thread = threading . Thread ( target = do_it )
thread . daemon = True
thread . start ( )
2017-09-20 19:47:31 +00:00
def LaunchFile ( path , launch_path = None ) :
2015-11-04 22:30:28 +00:00
2017-09-20 19:47:31 +00:00
def do_it ( launch_path ) :
2015-11-04 22:30:28 +00:00
2017-09-20 19:47:31 +00:00
if HC . PLATFORM_WINDOWS and launch_path is None :
2015-11-04 22:30:28 +00:00
os . startfile ( path )
else :
2017-09-20 19:47:31 +00:00
if launch_path is None :
2017-06-07 22:05:15 +00:00
2017-09-20 19:47:31 +00:00
launch_path = GetDefaultLaunchPath ( )
2017-06-07 22:05:15 +00:00
2017-04-12 21:46:46 +00:00
2019-01-09 22:59:03 +00:00
complete_launch_path = launch_path . replace ( ' % path % ' , path )
2015-11-04 22:30:28 +00:00
2019-05-15 20:35:00 +00:00
hide_terminal = False
2017-09-20 19:47:31 +00:00
if HC . PLATFORM_WINDOWS :
2019-01-09 22:59:03 +00:00
cmd = complete_launch_path
2019-05-15 20:35:00 +00:00
2017-09-20 19:47:31 +00:00
preexec_fn = None
else :
2019-01-09 22:59:03 +00:00
cmd = shlex . split ( complete_launch_path )
2017-09-20 19:47:31 +00:00
2020-03-25 21:15:57 +00:00
preexec_fn = getattr ( os , ' setsid ' , None )
2017-09-20 19:47:31 +00:00
2015-11-04 22:30:28 +00:00
2019-02-13 22:26:43 +00:00
if HG . subprocess_report_mode :
2018-08-01 20:44:57 +00:00
message = ' Attempting to launch ' + path + ' using command ' + repr ( cmd ) + ' . '
2019-02-13 22:26:43 +00:00
HydrusData . ShowText ( message )
2018-08-01 20:44:57 +00:00
2017-09-20 19:47:31 +00:00
try :
2019-05-15 20:35:00 +00:00
sbp_kwargs = HydrusData . GetSubprocessKWArgs ( hide_terminal = hide_terminal , text = True )
2019-01-09 22:59:03 +00:00
2020-06-17 21:31:54 +00:00
HydrusData . CheckProgramIsNotShuttingDown ( )
2019-01-16 22:40:53 +00:00
process = subprocess . Popen ( cmd , preexec_fn = preexec_fn , stdin = subprocess . PIPE , stdout = subprocess . PIPE , stderr = subprocess . PIPE , * * sbp_kwargs )
2017-09-20 19:47:31 +00:00
2020-06-17 21:31:54 +00:00
( stdout , stderr ) = HydrusThreading . SubprocessCommunicate ( process )
2018-04-11 22:30:40 +00:00
2019-02-13 22:26:43 +00:00
if HG . subprocess_report_mode :
2018-04-18 22:10:15 +00:00
if stdout is None and stderr is None :
HydrusData . ShowText ( ' No stdout or stderr came back. ' )
2018-04-11 22:30:40 +00:00
if stdout is not None :
2018-04-18 22:10:15 +00:00
HydrusData . ShowText ( ' stdout: ' + repr ( stdout ) )
2018-04-11 22:30:40 +00:00
if stderr is not None :
2018-04-18 22:10:15 +00:00
HydrusData . ShowText ( ' stderr: ' + repr ( stderr ) )
2018-04-11 22:30:40 +00:00
2017-09-20 19:47:31 +00:00
except Exception as e :
2019-01-09 22:59:03 +00:00
HydrusData . ShowText ( ' Could not launch a file! Command used was: ' + os . linesep + str ( cmd ) )
2017-09-20 19:47:31 +00:00
HydrusData . ShowException ( e )
2015-11-04 22:30:28 +00:00
2017-09-20 19:47:31 +00:00
thread = threading . Thread ( target = do_it , args = ( launch_path , ) )
2015-11-04 22:30:28 +00:00
thread . daemon = True
thread . start ( )
2016-08-17 20:07:22 +00:00
def MakeSureDirectoryExists ( path ) :
2022-08-03 20:59:51 +00:00
it_exists_already = os . path . exists ( path )
if it_exists_already :
if os . path . isdir ( path ) :
return
else :
raise Exception ( ' Sorry, the desired directory " {} " already exists as a normal file! ' . format ( path ) )
2019-05-08 21:06:42 +00:00
os . makedirs ( path , exist_ok = True )
2016-08-17 20:07:22 +00:00
2021-10-27 21:12:33 +00:00
def safe_copy2 ( source , dest ) :
2016-06-08 20:27:22 +00:00
2021-10-27 21:12:33 +00:00
copy_metadata = True
if HC . PLATFORM_WINDOWS :
2017-10-25 21:45:15 +00:00
2021-10-27 21:12:33 +00:00
mtime = os . path . getmtime ( source )
# this is 1980-01-01 UTC, before which Windows can have trouble copying lmaoooooo
if mtime < 315532800 :
copy_metadata = False
2017-10-25 21:45:15 +00:00
2021-10-27 21:12:33 +00:00
if copy_metadata :
2016-08-17 20:07:22 +00:00
2021-10-27 21:12:33 +00:00
# this overwrites on conflict without hassle
shutil . copy2 ( source , dest )
2016-08-17 20:07:22 +00:00
else :
2016-06-08 20:27:22 +00:00
2021-10-27 21:12:33 +00:00
shutil . copy ( source , dest )
def MergeFile ( source , dest ) :
# this can merge a file, but if it is given a dir it will just straight up overwrite not merge
2023-03-29 20:57:59 +00:00
if os . path . exists ( source ) and os . path . exists ( dest ) and os . path . samefile ( source , dest ) :
raise Exception ( f ' Woah, " { source } " and " { dest } " are the same file! ' )
2021-10-27 21:12:33 +00:00
if not os . path . isdir ( source ) :
if PathsHaveSameSizeAndDate ( source , dest ) :
2016-06-08 20:27:22 +00:00
2021-10-27 21:12:33 +00:00
DeletePath ( source )
2016-06-08 20:27:22 +00:00
2021-10-27 21:12:33 +00:00
return True
2016-06-08 20:27:22 +00:00
2021-10-27 21:12:33 +00:00
try :
# this overwrites on conflict without hassle
shutil . move ( source , dest , copy_function = safe_copy2 )
except Exception as e :
HydrusData . ShowText ( ' Trying to move ' + source + ' to ' + dest + ' caused the following problem: ' )
HydrusData . ShowException ( e )
return False
2016-06-08 20:27:22 +00:00
return True
2023-03-29 20:57:59 +00:00
2017-08-02 21:32:54 +00:00
def MergeTree ( source , dest , text_update_hook = None ) :
2016-08-17 20:07:22 +00:00
2023-03-29 20:57:59 +00:00
if os . path . exists ( source ) and os . path . exists ( dest ) and os . path . samefile ( source , dest ) :
raise Exception ( f ' Woah, " { source } " and " { dest } " are the same directory! ' )
2016-08-17 20:07:22 +00:00
pauser = HydrusData . BigJobPauser ( )
if not os . path . exists ( dest ) :
2017-10-25 21:45:15 +00:00
try :
2021-10-27 21:12:33 +00:00
shutil . move ( source , dest , copy_function = safe_copy2 )
2017-10-25 21:45:15 +00:00
except OSError :
# if there were read only files in source and this was partition to partition, the copy2 goes ok but the subsequent source unlink fails
# so, if it seems this has happened, let's just try a walking mergetree, which should be able to deal with these readonlies on a file-by-file basis
if os . path . exists ( dest ) :
MergeTree ( source , dest , text_update_hook = text_update_hook )
2016-08-17 20:07:22 +00:00
else :
2021-10-27 21:12:33 +00:00
# I had a thing here that tried to optimise if dest existed but was empty, but it wasn't neat
num_errors = 0
for ( root , dirnames , filenames ) in os . walk ( source ) :
2016-08-17 20:07:22 +00:00
2021-10-27 21:12:33 +00:00
if text_update_hook is not None :
2016-08-17 20:07:22 +00:00
2021-10-27 21:12:33 +00:00
text_update_hook ( ' Copying ' + root + ' . ' )
2016-08-17 20:07:22 +00:00
2021-10-27 21:12:33 +00:00
dest_root = root . replace ( source , dest )
2017-08-02 21:32:54 +00:00
2021-10-27 21:12:33 +00:00
for dirname in dirnames :
pauser . Pause ( )
source_path = os . path . join ( root , dirname )
dest_path = os . path . join ( dest_root , dirname )
MakeSureDirectoryExists ( dest_path )
shutil . copystat ( source_path , dest_path )
2017-08-02 21:32:54 +00:00
2021-10-27 21:12:33 +00:00
for filename in filenames :
2016-08-17 20:07:22 +00:00
2021-10-27 21:12:33 +00:00
if num_errors > 5 :
2016-08-17 20:07:22 +00:00
2021-10-27 21:12:33 +00:00
raise Exception ( ' Too many errors, directory move abandoned. ' )
2016-08-17 20:07:22 +00:00
2021-10-27 21:12:33 +00:00
pauser . Pause ( )
2016-08-17 20:07:22 +00:00
2021-10-27 21:12:33 +00:00
source_path = os . path . join ( root , filename )
dest_path = os . path . join ( dest_root , filename )
2016-08-17 20:07:22 +00:00
2021-10-27 21:12:33 +00:00
ok = MergeFile ( source_path , dest_path )
if not ok :
2017-08-02 21:32:54 +00:00
2021-10-27 21:12:33 +00:00
num_errors + = 1
2016-08-17 20:07:22 +00:00
2021-10-27 21:12:33 +00:00
if num_errors == 0 :
DeletePath ( source )
2017-03-02 02:14:56 +00:00
2016-08-17 20:07:22 +00:00
2023-03-29 20:57:59 +00:00
2016-06-01 20:04:15 +00:00
def MirrorFile ( source , dest ) :
2023-03-29 20:57:59 +00:00
if os . path . exists ( source ) and os . path . exists ( dest ) and os . path . samefile ( source , dest ) :
return True
2016-06-01 20:04:15 +00:00
if not PathsHaveSameSizeAndDate ( source , dest ) :
try :
2022-01-12 22:14:50 +00:00
TryToMakeFileWriteable ( dest )
2018-06-06 21:27:02 +00:00
2021-10-27 21:12:33 +00:00
safe_copy2 ( source , dest )
2016-06-01 20:04:15 +00:00
except Exception as e :
HydrusData . ShowText ( ' Trying to copy ' + source + ' to ' + dest + ' caused the following problem: ' )
HydrusData . ShowException ( e )
return False
return True
2017-07-12 20:03:45 +00:00
def MirrorTree ( source , dest , text_update_hook = None , is_cancelled_hook = None ) :
2016-01-06 21:17:20 +00:00
2023-03-29 20:57:59 +00:00
if os . path . exists ( source ) and os . path . exists ( dest ) and os . path . samefile ( source , dest ) :
return
2016-01-06 21:17:20 +00:00
pauser = HydrusData . BigJobPauser ( )
2016-08-17 20:07:22 +00:00
MakeSureDirectoryExists ( dest )
2016-01-06 21:17:20 +00:00
2016-06-01 20:04:15 +00:00
num_errors = 0
2016-01-06 21:17:20 +00:00
for ( root , dirnames , filenames ) in os . walk ( source ) :
2017-07-12 20:03:45 +00:00
if is_cancelled_hook is not None and is_cancelled_hook ( ) :
return
if text_update_hook is not None :
text_update_hook ( ' Copying ' + root + ' . ' )
2016-01-06 21:17:20 +00:00
dest_root = root . replace ( source , dest )
surplus_dest_paths = { os . path . join ( dest_root , dest_filename ) for dest_filename in os . listdir ( dest_root ) }
for dirname in dirnames :
pauser . Pause ( )
source_path = os . path . join ( root , dirname )
dest_path = os . path . join ( dest_root , dirname )
surplus_dest_paths . discard ( dest_path )
2016-08-17 20:07:22 +00:00
MakeSureDirectoryExists ( dest_path )
2016-01-06 21:17:20 +00:00
shutil . copystat ( source_path , dest_path )
for filename in filenames :
2016-06-01 20:04:15 +00:00
if num_errors > 5 :
raise Exception ( ' Too many errors, directory copy abandoned. ' )
2016-01-06 21:17:20 +00:00
pauser . Pause ( )
source_path = os . path . join ( root , filename )
dest_path = os . path . join ( dest_root , filename )
surplus_dest_paths . discard ( dest_path )
2016-06-01 20:04:15 +00:00
ok = MirrorFile ( source_path , dest_path )
if not ok :
2016-01-06 21:17:20 +00:00
2016-06-01 20:04:15 +00:00
num_errors + = 1
2016-01-06 21:17:20 +00:00
for dest_path in surplus_dest_paths :
pauser . Pause ( )
DeletePath ( dest_path )
2023-03-29 20:57:59 +00:00
2017-06-07 22:05:15 +00:00
def OpenFileLocation ( path ) :
def do_it ( ) :
if HC . PLATFORM_WINDOWS :
2019-01-09 22:59:03 +00:00
cmd = [ ' explorer ' , ' /select, ' , path ]
2017-06-07 22:05:15 +00:00
2019-11-20 23:10:46 +00:00
elif HC . PLATFORM_MACOS :
2017-06-07 22:05:15 +00:00
2019-01-09 22:59:03 +00:00
cmd = [ ' open ' , ' -R ' , path ]
2017-06-07 22:05:15 +00:00
elif HC . PLATFORM_LINUX :
2017-12-13 22:33:07 +00:00
raise NotImplementedError ( ' Linux cannot open file locations! ' )
2017-06-07 22:05:15 +00:00
2020-07-15 20:52:09 +00:00
elif HC . PLATFORM_HAIKU :
raise NotImplementedError ( ' Haiku cannot open file locations! ' )
2017-06-07 22:05:15 +00:00
2019-01-09 22:59:03 +00:00
sbp_kwargs = HydrusData . GetSubprocessKWArgs ( hide_terminal = False )
2020-06-17 21:31:54 +00:00
HydrusData . CheckProgramIsNotShuttingDown ( )
2019-01-09 22:59:03 +00:00
process = subprocess . Popen ( cmd , * * sbp_kwargs )
2017-06-07 22:05:15 +00:00
2020-06-17 21:31:54 +00:00
HydrusThreading . SubprocessCommunicate ( process )
2017-06-07 22:05:15 +00:00
thread = threading . Thread ( target = do_it )
thread . daemon = True
thread . start ( )
2016-01-06 21:17:20 +00:00
def PathsHaveSameSizeAndDate ( path1 , path2 ) :
if os . path . exists ( path1 ) and os . path . exists ( path2 ) :
2016-04-14 01:54:29 +00:00
same_size = os . path . getsize ( path1 ) == os . path . getsize ( path2 )
2016-05-18 20:07:14 +00:00
same_modified_time = int ( os . path . getmtime ( path1 ) ) == int ( os . path . getmtime ( path2 ) )
2016-01-06 21:17:20 +00:00
if same_size and same_modified_time :
return True
2018-01-24 23:09:42 +00:00
return False
def PathIsFree ( path ) :
try :
2019-11-20 23:10:46 +00:00
stat_result = os . stat ( path )
current_bits = stat_result . st_mode
if not current_bits & stat . S_IWRITE :
# read-only file, cannot do the rename check
return True
2018-01-24 23:09:42 +00:00
os . rename ( path , path ) # rename a path to itself
return True
2018-09-26 19:05:12 +00:00
except OSError as e : # 'already in use by another process' or an odd filename too long error
2018-01-24 23:09:42 +00:00
2018-09-26 19:05:12 +00:00
HydrusData . Print ( ' Already in use/inaccessible: ' + path )
2018-01-24 23:09:42 +00:00
2016-01-06 21:17:20 +00:00
return False
2015-11-04 22:30:28 +00:00
def ReadFileLikeAsBlocks ( f ) :
next_block = f . read ( HC . READ_BLOCK_SIZE )
2019-01-09 22:59:03 +00:00
while len ( next_block ) > 0 :
2015-11-04 22:30:28 +00:00
yield next_block
next_block = f . read ( HC . READ_BLOCK_SIZE )
2015-11-25 22:00:57 +00:00
def RecyclePath ( path ) :
2019-07-24 21:39:02 +00:00
if HG . file_report_mode :
HydrusData . ShowText ( ' Recycling {} ' . format ( path ) )
HydrusData . ShowText ( ' ' . join ( traceback . format_stack ( ) ) )
2015-11-25 22:00:57 +00:00
if os . path . exists ( path ) :
2022-01-12 22:14:50 +00:00
TryToMakeFileWriteable ( path )
2016-05-04 21:50:55 +00:00
2015-11-25 22:00:57 +00:00
try :
send2trash . send2trash ( path )
except :
2016-06-08 20:27:22 +00:00
HydrusData . Print ( ' Trying to recycle ' + path + ' created this error: ' )
2016-10-19 20:02:56 +00:00
HydrusData . DebugPrint ( traceback . format_exc ( ) )
2015-11-25 22:00:57 +00:00
HydrusData . Print ( ' It has been fully deleted instead. ' )
2019-01-09 22:59:03 +00:00
DeletePath ( path )
2015-11-25 22:00:57 +00:00
2017-01-18 22:52:39 +00:00
2022-07-27 21:18:33 +00:00
def SanitizeFilename ( filename , force_ntfs = False ) - > str :
2018-09-26 19:05:12 +00:00
2022-07-27 21:18:33 +00:00
if HC . PLATFORM_WINDOWS or force_ntfs :
2018-09-26 19:05:12 +00:00
# \, /, :, *, ?, ", <, >, |
2022-07-27 21:18:33 +00:00
bad_characters = r ' [ \\ /:*? " <>|] '
2018-09-26 19:05:12 +00:00
else :
2022-07-27 21:18:33 +00:00
bad_characters = ' / '
2018-09-26 19:05:12 +00:00
2022-07-27 21:18:33 +00:00
return re . sub ( bad_characters , ' _ ' , filename )
def SanitizePathForExport ( directory_path , directories_and_filename ) :
# this does not figure out the situation where the suffix directories cross a mount point to a new file system, but at that point it is user's job to fix
2018-09-26 19:05:12 +00:00
2022-07-27 21:18:33 +00:00
components = directories_and_filename . split ( os . path . sep )
filename = components [ - 1 ]
suffix_directories = components [ : - 1 ]
force_ntfs = GetFileSystemType ( directory_path ) . lower ( ) in ( ' ntfs ' , ' exfat ' )
suffix_directories = [ SanitizeFilename ( suffix_directory , force_ntfs = force_ntfs ) for suffix_directory in suffix_directories ]
filename = SanitizeFilename ( filename , force_ntfs = force_ntfs )
sanitized_components = suffix_directories
sanitized_components . append ( filename )
return os . path . join ( * sanitized_components )
2022-01-12 22:14:50 +00:00
def TryToGiveFileNicePermissionBits ( path ) :
if not os . path . exists ( path ) :
return
try :
stat_result = os . stat ( path )
current_bits = stat_result . st_mode
if HC . PLATFORM_WINDOWS :
# this is actually the same value as S_IWUSR, but let's not try to second guess ourselves
desired_bits = stat . S_IREAD | stat . S_IWRITE
else :
# typically guarantee 644 for regular files m8, but now we also take umask into account
try :
umask = os . umask ( 0o022 )
os . umask ( umask )
except :
umask = 0o022
desired_bits = ( stat . S_IRUSR | stat . S_IWUSR | stat . S_IRGRP | stat . S_IROTH ) & ~ umask
if not ( desired_bits & current_bits ) == desired_bits :
os . chmod ( path , current_bits | desired_bits )
except Exception as e :
HydrusData . Print ( ' Wanted to add read and write permission to " {} " , but had an error: {} ' . format ( path , str ( e ) ) )
def TryToMakeFileWriteable ( path ) :
if not os . path . exists ( path ) :
return
if FileisWriteable ( path ) :
return
try :
stat_result = os . stat ( path )
current_bits = stat_result . st_mode
if HC . PLATFORM_WINDOWS :
# this is actually the same value as S_IWUSR, but let's not try to second guess ourselves
desired_bits = stat . S_IREAD | stat . S_IWRITE
else :
# this only does what we want if we own the file, but only owners can non-sudo change permission anyway
desired_bits = stat . S_IWUSR
if not ( desired_bits & current_bits ) == desired_bits :
os . chmod ( path , current_bits | desired_bits )
except Exception as e :
HydrusData . Print ( ' Wanted to add user write permission to " {} " , but had an error: {} ' . format ( path , str ( e ) ) )