/
extfix.py
executable file
·142 lines (115 loc) · 4.5 KB
/
extfix.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
#!/usr/bin/env python3
# System libraries
import argparse
import csv
import logging
from pathlib import Path
# External libraries
import magic as fm
log = logging.getLogger(__name__)
with open('extensions.csv', mode='r') as file:
reader = csv.reader(file)
exts_dict = {rows[0]:rows[1] for rows in reader}
class BadFilenameException(Exception):
pass
class NotInDictionaryException(Exception):
pass
class ArgumentException(Exception):
pass
# если создать массив через фигурные скобки,
# типа как словарь без значений, то получится множество (set),
# к которому операция `x in set(...)` проходит гораздо быстрее
FILENAME_PROHIBITED = {"<", ">", ":", '"', "\\", "|", "?", "*"}
class FileFixer:
def __init__(self, file: Path):
self.file = file
if any(i in file.name for i in FILENAME_PROHIBITED):
raise BadFilenameException(file)
def current_extension(self) -> str:
return self.file.suffix[1:]
def real_extension(self) -> str:
if self.file.is_dir():
raise IsADirectoryError(self.file)
mime = fm.detect_from_filename(self.file).mime_type
true_ext = exts_dict.get(mime)
if true_ext:
log.info("Real extension: %s", exts_dict[mime])
return true_ext
raise NotInDictionaryException(self.file)
def extension_correct(self):
"""
Returns True if extension is correct,
otherwise returns real extension.
"""
real_ext = self.real_extension()
if real_ext == "blacklisted" or real_ext == self.current_extension():
return True
return real_ext
def change_ext(self, ext: str):
log.info("Processing file %s", self.file)
ext = self.real_extension()
file_noext = self.file.stem # имя файла, без последнего расширения
newpath = self.file.parent / f"{file_noext}.{ext}"
log.info("Renaming to %s", newpath.name)
self.file.rename(newpath)
def fix(self):
real = self.extension_correct()
if real is not True:
self.change_ext(real)
# для корректного отображения объекта при дебаге
def __repr__(self):
return f"FileFixer({self.file!r})"
def recursive_dirlist_builder(path, arr, count):
# вообще, мне не нравятся эти приколы с arr и ps.
# TODO как-нибудь причесать это к одному массиву
ps = []
for child in path.iterdir():
if child.is_dir():
log.info("Appending %s directory to the search list", child)
ps.append(child)
arr.extend(ps)
if not count or not ps:
return arr
else:
for x in ps:
log.info("Going into folder %s", x)
return recursive_dirlist_builder(x, arr, count - 1)
def fix_all(directories):
files = []
for dir in directories:
for child in dir.iterdir():
if child.is_dir():
continue
log.info("Appending %s file to the search list", child)
files.append(child)
for file in files:
FileFixer(file).fix()
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Utility to fix your files' extensions."
)
parser.add_argument("paths", type=Path, nargs="+", help="Path argument(s)")
parser.add_argument(
"-d",
"--depth",
type=int,
default=1,
help="depth of recursion for directories (default is 1)",
)
parser.add_argument(
"-v", "--verbose", action="store_true", help="shows changes in your files"
)
args = parser.parse_args()
# формат сообщений в логе. полный перечень переменных здесь:
# https://docs.python.org/3/library/logging.html#logrecord-attributes
log_format = "%(levelname)s %(message)s"
logging.basicConfig(format=log_format)
# если передали --verbose, то логгер выводит
# все сообщения уровнем INFO и выше,
# в остальных случаях сообщаем только о WARN и ERROR
log.setLevel(logging.INFO if args.verbose else logging.WARN)
arr = args.paths.copy()
for x in args.paths:
arr.extend(recursive_dirlist_builder(x, [], args.depth))
fix_all(arr)
log.info("Done.")