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+
323import os
4- import hashlib
24+ import sys
525import json
626import 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-
2147def 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-
2859def 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
90178if __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 ()
0 commit comments