Skip to content

Commit 445a389

Browse files
committed
Update
1 parent 0985c57 commit 445a389

6 files changed

Lines changed: 412 additions & 394 deletions

File tree

Lines changed: 154 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,100 +1,179 @@
11
#!/data/data/com.tool.tree/files/home/termux/bin/python
22

3+
# Tóm tắt:
4+
# Script kiểm tra các thay đổi của file trong thư mục lớn trên Android/Termux.
5+
# - Dùng ThreadPoolExecutor (không sử dụng multiprocessing.SemLock).
6+
# - Quét file theo luồng (generator) để giảm tiêu thụ bộ nhớ.
7+
# - Lưu kết quả hash, kích thước, thời gian chỉnh sửa vào file JSON.
8+
# - Bỏ qua hash file khi kích thước và thời gian không đổi (tiết kiệm thời gian).
9+
# - Đọc file với bộ đệm 1 MB.
10+
# - Số luồng tối đa mặc định: min(32, CPU*2). Cho phép cấu hình qua tùy chọn --workers.
11+
# - Loại trừ đuôi file mặc định: .bak, .tmp, .log. Có thể tùy chỉnh qua --exclude.
12+
# - Có tùy chọn --force để tính toán lại tất cả file.
13+
# - Lưu file database một cách nguyên tử (ghi vào tạm rồi replace).
14+
# - Xử lý đường dẫn Unicode và lỗi I/O một cách chắc chắn.
15+
# - In ra thay đổi theo định dạng: new:, fixed:, deleted:.
16+
# - Bảng tham số khuyến nghị:
17+
# Buffer đọc | Số luồng tối đa | Chunk size (executor.map)
18+
# 1048576 (1 MB) | min(32, CPU*2) | 64
19+
#
20+
# Ví dụ sử dụng:
21+
# python script.py /duong/dan/thu_muc data.json --exclude=.bak,.tmp --force
22+
323
import os
4-
import hashlib
24+
import sys
525
import json
626
import argparse
7-
import multiprocessing as mp
8-
27+
import hashlib
28+
from concurrent.futures import ThreadPoolExecutor, as_completed
929

10-
def compute_hash(file_path):
30+
def compute_hash(file_path, buffer_size=1048576):
31+
"""
32+
Tính SHA-256 của file.
33+
Trả về tuple (file_path, hexhash) hoặc (file_path, None) nếu có lỗi.
34+
"""
1135
sha256 = hashlib.sha256()
1236
try:
1337
with open(file_path, "rb") as f:
14-
for chunk in iter(lambda: f.read(65536), b""):
38+
while True:
39+
chunk = f.read(buffer_size)
40+
if not chunk:
41+
break
1542
sha256.update(chunk)
1643
return file_path, sha256.hexdigest()
1744
except Exception:
1845
return file_path, None
1946

20-
2147
def load_hash_db(hash_db_path):
48+
"""
49+
Đọc database JSON (nếu có) trả về dict {rel_path: {"hash":..., "size":..., "mtime":...}}.
50+
"""
2251
if os.path.exists(hash_db_path):
23-
with open(hash_db_path, "r") as f:
24-
return json.load(f)
52+
try:
53+
with open(hash_db_path, "r", encoding="utf-8") as f:
54+
return json.load(f)
55+
except Exception:
56+
return {}
2557
return {}
2658

27-
2859
def save_hash_db(hash_db_path, hash_db):
29-
with open(hash_db_path, "w") as f:
30-
json.dump(hash_db, f, indent=2)
31-
32-
33-
def collect_files(root_dir, excluded_exts):
34-
files = []
35-
36-
for dirpath, _, filenames in os.walk(root_dir):
37-
for filename in filenames:
38-
39-
if filename.endswith(tuple(excluded_exts)):
40-
continue
41-
42-
path = os.path.join(dirpath, filename)
43-
files.append(path)
44-
45-
return files
46-
47-
48-
def scan_directory(root_dir, excluded_exts):
49-
50-
files = collect_files(root_dir, excluded_exts)
51-
52-
cpu = mp.cpu_count()
53-
54-
current_hashes = {}
55-
56-
with mp.Pool(cpu) as pool:
57-
58-
for file_path, file_hash in pool.imap_unordered(compute_hash, files):
59-
60-
if file_hash:
60+
"""
61+
Lưu database JSON một cách nguyên tử: ghi tạm, rồi replace.
62+
"""
63+
temp_file = hash_db_path + ".tmp"
64+
try:
65+
with open(temp_file, "w", encoding="utf-8") as f:
66+
json.dump(hash_db, f, ensure_ascii=False, indent=2)
67+
os.replace(temp_file, hash_db_path)
68+
except Exception as e:
69+
print(f"Unable to save database hash: {e}", file=sys.stderr)
70+
71+
def iter_files(root_dir, excluded_exts):
72+
"""
73+
Yield đường dẫn file trong thư mục (không đệ quy).
74+
Loại trừ các file có đuôi trong excluded_exts.
75+
"""
76+
for entry in os.scandir(root_dir):
77+
if not entry.is_file():
78+
continue
79+
name = entry.name
80+
if any(name.lower().endswith(ext) for ext in excluded_exts):
81+
continue
82+
yield entry.path
83+
84+
def scan_directory(root_dir, excluded_exts, previous_db, force=False, buffer_size=1048576, max_workers=None):
85+
"""
86+
Quét thư mục, tính hash cho file mới/đã thay đổi, kết hợp với database trước đó.
87+
Trả về dict mới {rel_path: {"hash":..., "size":..., "mtime":...}}.
88+
"""
89+
current_db = {}
90+
# Chuẩn bị công việc: xác định file cần hash (generator)
91+
def gen_tasks():
92+
for file_path in iter_files(root_dir, excluded_exts):
93+
rel = os.path.relpath(file_path, root_dir)
94+
try:
95+
st = os.stat(file_path)
96+
except Exception:
97+
continue # bỏ qua file không thể truy xuất
98+
size = st.st_size
99+
mtime = st.st_mtime_ns
100+
prev = previous_db.get(rel)
101+
if not force and prev and prev.get("size") == size and prev.get("mtime") == mtime:
102+
# Không thay đổi, dùng hash cũ
103+
current_db[rel] = {"hash": prev.get("hash"), "size": size, "mtime": mtime}
104+
else:
105+
# Cần tính hash
106+
yield file_path
107+
108+
if max_workers is None:
109+
cpu = os.cpu_count() or 1
110+
max_workers = min(32, cpu * 2)
111+
try:
112+
with ThreadPoolExecutor(max_workers=max_workers) as executor:
113+
# executor.map với chunksize giới hạn số tasks đồng thời
114+
for file_path, file_hash in executor.map(lambda p: compute_hash(p, buffer_size), gen_tasks(), chunksize=64):
61115
rel = os.path.relpath(file_path, root_dir)
62-
current_hashes[rel] = file_hash
63-
64-
return current_hashes
65-
66-
67-
def check_changes(root_dir, hash_db_path):
68-
69-
excluded_extensions = [".bak", ".tmp", ".log"]
70-
71-
previous_hashes = load_hash_db(hash_db_path)
72-
73-
current_hashes = scan_directory(root_dir, excluded_extensions)
74-
75-
for rel_path, current_hash in current_hashes.items():
76-
77-
if rel_path not in previous_hashes:
78-
print(f"new: {rel_path}")
79-
80-
elif previous_hashes[rel_path] != current_hash:
81-
print(f"fixed: {rel_path}")
116+
try:
117+
st = os.stat(file_path)
118+
size = st.st_size
119+
mtime = st.st_mtime_ns
120+
except Exception:
121+
size = None
122+
mtime = None
123+
if file_hash:
124+
current_db[rel] = {"hash": file_hash, "size": size, "mtime": mtime}
125+
else:
126+
current_db[rel] = {"hash": None, "size": size, "mtime": mtime}
127+
except KeyboardInterrupt:
128+
print("Program paused (interrupted by user)...", file=sys.stderr)
129+
return current_db
130+
131+
return current_db
132+
133+
def check_changes(root_dir, hash_db_path, exclude_list, force=False, buffer_size=1048576, max_workers=None):
134+
"""
135+
So sánh thay đổi giữa hash cũ và hash mới, in ra kết quả.
136+
Cập nhật và lưu database.
137+
"""
138+
previous_db = load_hash_db(hash_db_path)
139+
current_db = scan_directory(root_dir, exclude_list, previous_db, force, buffer_size, max_workers)
140+
141+
# In kết quả so sánh
142+
for rel, info in current_db.items():
143+
if rel not in previous_db:
144+
print(f"new: {rel}")
145+
elif previous_db.get(rel, {}).get("hash") != info.get("hash"):
146+
print(f"fixed: {rel}")
147+
for rel in previous_db:
148+
if rel not in current_db:
149+
print(f"deleted: {rel}")
150+
151+
# Lưu database mới
152+
save_hash_db(hash_db_path, current_db)
153+
154+
def main():
155+
parser = argparse.ArgumentParser(description="Scan the directory, calculate the SHA-256 value, and check for changes.")
156+
parser.add_argument("directory", help="Folder to scan")
157+
parser.add_argument("hash_db", help="The path to the JSON file storing the cache hash.")
158+
parser.add_argument("--exclude", default=".bak,.tmp,.log",
159+
help="Excluded file extensions (separated by commas). Default: .bak, .tmp, .log")
160+
parser.add_argument("--force", action="store_true",
161+
help="Recalculate the hash for all files, without using the cache.")
162+
parser.add_argument("--workers", type=int,
163+
help="Maximum number of threads (default = min(32, CPU*2))")
164+
args = parser.parse_args()
82165

83-
for rel_path in previous_hashes:
84-
if rel_path not in current_hashes:
85-
print(f"deleted: {rel_path}")
166+
directory = os.path.abspath(args.directory)
167+
exclude_exts = [ext if ext.startswith('.') else '.'+ext for ext in (args.exclude or "").split(',') if ext.strip()]
168+
if not exclude_exts:
169+
exclude_exts = [".bak", ".tmp", ".log"]
86170

87-
save_hash_db(hash_db_path, current_hashes)
171+
if not os.path.isdir(directory):
172+
print(f"Paths that are not folders: {directory}", file=sys.stderr)
173+
sys.exit(1)
88174

175+
check_changes(directory, args.hash_db, exclude_exts, force=args.force,
176+
buffer_size=1048576, max_workers=args.workers)
89177

90178
if __name__ == "__main__":
91-
92-
parser = argparse.ArgumentParser()
93-
94-
parser.add_argument("directory")
95-
96-
parser.add_argument("hash_db")
97-
98-
args = parser.parse_args()
99-
100-
check_changes(args.directory, args.hash_db)
179+
main()

.github/module/termux/py/gettype.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import os
44
import sys
5-
import multiprocessing as mp
5+
from concurrent.futures import ThreadPoolExecutor, as_completed
66

77
formats = (
88
(b'PK', "zip"),
@@ -185,7 +185,6 @@ def main(path):
185185
path = os.path.abspath(path)
186186
cache = load_cache()
187187

188-
# Thư mục gốc đang chạy
189188
if os.path.isfile(path):
190189
root = os.path.dirname(path)
191190
root = os.path.abspath(root)
@@ -204,11 +203,13 @@ def main(path):
204203
return
205204

206205
valid_relpaths = set()
206+
cpu = min((os.cpu_count() or 1), 4)
207+
args = [(f, root, root_cache) for f in files]
207208

208-
cpu = min(mp.cpu_count(), 4)
209-
with mp.Pool(cpu) as pool:
210-
args = [(f, root, root_cache) for f in files]
211-
for file, relpath, typ, mtime_ns in pool.imap_unordered(gettype, args):
209+
with ThreadPoolExecutor(max_workers=cpu) as executor:
210+
futures = [executor.submit(gettype, arg) for arg in args]
211+
for future in as_completed(futures):
212+
file, relpath, typ, mtime_ns = future.result()
212213
valid_relpaths.add(relpath)
213214
cache.setdefault(root, {})[relpath] = (typ, mtime_ns)
214215
print(f"{os.path.basename(file)}:{typ}")
@@ -221,4 +222,4 @@ def main(path):
221222
if len(sys.argv) < 2:
222223
print("Usage: gettype.py <file_or_directory>")
223224
sys.exit(1)
224-
main(sys.argv[1])
225+
main(sys.argv[1])

0 commit comments

Comments
 (0)