-
Notifications
You must be signed in to change notification settings - Fork 0
/
Checks.py
185 lines (156 loc) · 6.22 KB
/
Checks.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
import logging
import os
import random
import re
import shutil
import subprocess
from pathlib import Path
from psutil import disk_partitions
log = logging.getLogger(__name__)
class Checks:
# regex for illegal characters in file names and database queries
fix_linux = re.compile(r"[/]|[\x00-\x1f]|\x7f|\x00")
fix_windows = re.compile(r'[,<>:"/\\|?*]|[\x00-\x1f]|\x7f|\x00')
fix_windows_ending = re.compile("([ .]+$)")
fix_whitespace_ending = re.compile("([ \t]+$)")
fix_unicode = re.compile(r"[^\x00-\x7F]")
# these filesystem types will have NTFS style filename restrictions
windows_fs = ["fat", "ntfs", "9p"]
WINDOWS_MAX_PATH = 248
def __init__(self, root_path: Path, max_filename, ntfs):
self.root_path: Path = root_path
self._root_str: str = str(root_path).lower()
if ntfs:
self.is_linux: bool = False
else:
self.is_linux: bool = self._check_linux_filesystem()
self.is_symlink: bool = self._symlinks_supported()
self.is_unicode: bool = self._unicode_filenames()
self.is_case_sensitive: bool = self._check_case_sensitive()
self.max_path: int = self._get_max_path_length()
if max_filename > 0:
self.max_filename: int = max_filename
else:
self.max_filename: int = self._get_max_filename_length()
def _check_linux_filesystem(self) -> bool:
filesystem_type = ""
for part in disk_partitions(True):
if part.mountpoint == "/":
filesystem_type = part.fstype
continue
if self._root_str.startswith(part.mountpoint.lower()):
filesystem_type = part.fstype
break
filesystem_type = filesystem_type.lower()
is_linux = not any(fs in filesystem_type for fs in self.windows_fs)
log.info(f"Target filesystem {self._root_str} is {filesystem_type}")
return is_linux
def _symlinks_supported(self) -> bool:
log.debug("Checking if is filesystem supports symbolic links...")
dst = "test_dst_%s" % random.getrandbits(32)
src = "test_src_%s" % random.getrandbits(32)
dst_file = self.root_path / dst
src_file = self.root_path / src
src_file.touch()
try:
log.debug("attempting to symlink %s to %s", src_file, dst_file)
dst_file.symlink_to(src_file)
dst_file.unlink()
src_file.unlink()
except BaseException:
if src_file.exists():
src_file.unlink()
log.error("Symbolic links not supported")
log.error("Albums are not going to be synced - requires symlinks")
return False
return True
def _unicode_filenames(self) -> bool:
log.debug("Checking if File system supports unicode filenames...")
testfile = self.root_path / ".unicode_test.\U0001f604"
is_unicode = False
try:
testfile.touch()
except BaseException:
log.info("Filesystem does not support Unicode filenames")
else:
log.info("Filesystem supports Unicode filenames")
is_unicode = True
testfile.unlink()
return is_unicode
def _check_case_sensitive(self) -> bool:
log.debug("Checking if File system is case insensitive...")
check_folder = self.root_path / ".gphotos_check"
case_file = check_folder / "Temp.Test"
no_case_file = check_folder / "TEMP.TEST"
is_sensitive = False
try:
check_folder.mkdir()
case_file.touch()
no_case_file.touch()
files = list(check_folder.glob("*"))
if len(files) != 2:
raise ValueError("separate case files not seen")
case_file.unlink()
no_case_file.unlink()
except (FileExistsError, FileNotFoundError, ValueError):
log.info("Case insensitive file system found")
else:
log.info("Case sensitive file system found")
is_sensitive = True
finally:
shutil.rmtree(check_folder)
return is_sensitive
def _get_max_path_length(self) -> int:
# safe windows length
max_length = self.WINDOWS_MAX_PATH
# found this on:
# https://stackoverflow.com/questions/32807560/how-do-i-get-in-python-the-maximum-filesystem-path-length-in-unix
try:
max_length = int(
subprocess.check_output(["getconf", "PATH_MAX", str(self.root_path)])
)
except BaseException:
# for failures choose a safe size for Windows filesystems
log.info(
f"cant determine max filepath length, defaulting to " f"{max_length}"
)
log.info("Max Path Length: %d" % max_length)
return max_length
def _get_max_filename_length(self) -> int:
# safe windows length
max_filename = self.WINDOWS_MAX_PATH
try:
info = os.statvfs(str(self.root_path))
max_filename = info.f_namemax
except BaseException:
# for failures choose a safe size for Windows filesystems
max_filename = 248
log.info(
f"cant determine max filename length, " f"defaulting to {max_filename}"
)
log.info("Max filename length: %d" % max_filename)
return max_filename
def valid_file_name(self, s: str) -> str:
"""
makes sure a string is valid for creating file names
:param (str) s: input string
:return: (str): sanitized string
"""
s = self.fix_whitespace_ending.sub("", s)
if self.is_linux:
s = self.fix_linux.sub("_", s)
else:
s = self.fix_windows.sub("_", s)
s = self.fix_windows_ending.split(s)[0]
if not self.is_unicode:
s = self.fix_unicode.sub("_", s)
return s
# a global for holding the current root folder check results
root_folder: Checks = None
# ugly global stuff to avoid passing Checks object everywhere
def do_check(root: Path, max_filename=0, ntfs=None):
global root_folder
root_folder = Checks(root, max_filename, ntfs)
return root_folder
def get_check():
return root_folder