1 package de.spiritscorp.datasync.model;
2
3 /*-
4 * Data Sync
5 *
6 * Copyright © 2022 The Spirit
7 * @email thespirit@spiritscorp.network
8 *
9 * This program is free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 3 of the License, or
12 * (at your option) any later version.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
17 * See the GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License
20 * along with this program. If not, see <http://www.gnu.org/licenses/>.
21 */
22
23 import java.nio.file.Path;
24 import java.util.ArrayList;
25 import java.util.Collections;
26 import java.util.List;
27 import java.util.Map;
28 import java.util.TreeMap;
29
30 import de.spiritscorp.datasync.ScanType;
31 import de.spiritscorp.datasync.controller.SyncJobContext;
32 import de.spiritscorp.datasync.io.Debug;
33 import de.spiritscorp.datasync.io.Logger;
34
35 /**
36 * Core controller class responsible for managing high-performance file synchronization,
37 * directory scanning, and backup operations.
38 * <br>
39 * <br>
40 * This class orchestrates the synchronization pipeline by utilizing multi-threaded
41 * file traversals to process source and destination structures simultaneously. It tracks
42 * file attributes, evaluates state deltas to isolate unique changes, detects duplicate
43 * files based on sizes or checksum configurations, and handles safe file transfers.
44 * <br>
45 * <br>
46 * To ensure data integrity, structural backups are handled via a strict two-phase execution
47 * model: clearing obsolete files first (with optional local trashbin staging) before transferring
48 * new payloads. Internal storage maps are wrapped in synchronized structures to maintain
49 * thread safety during parallel operations.
50 *
51 */
52 public class Model {
53
54 /** Internal lookup registry mapping absolute source paths to their metadata profiles. */
55 private final Map<Path, FileAttributes> sourceMap;
56 /** Internal lookup registry mapping absolute destination paths to their metadata profiles. */
57 private final Map<Path, FileAttributes> destMap;
58 /** Analytical core component tasked with deep directory evaluations and delta state indexing. */
59 private final FileAnalyzer analyzer;
60 /** Operational synchronization engine executing low-level deployment and file erasure routines. */
61 private final FileHandler handler;
62
63 /**
64 * Constructs a new Model controller instance and sets up the central tracking components.
65 * <br>
66 * <br>
67 * Initializes the internal system logger for transaction auditing and binds the
68 * reference maps used for storing file attribute states on the source and destination sides.
69 *
70 * @param logger the active system logger instance used for operational and debug tracking
71 * @param sourceMap the tracking map used to store and evaluate source file attributes
72 * @param destMap the tracking map used to store and evaluate destination file attributes
73 */
74 public Model( final Logger logger, final Map<Path, FileAttributes> sourceMap, final Map<Path, FileAttributes> destMap ) {
75 this.sourceMap = sourceMap;
76 this.destMap = destMap;
77 this.analyzer = new FileAnalyzer();
78 this.handler = new FileHandler( logger );
79 }
80
81 /**
82 * Creates a thread-safe, synchronized sorted map backed by a standard TreeMap.
83 * <br>
84 * <br>
85 * This helper method is crucial for concurrent environments where multiple threads
86 * need to read and write to the file mapping without risking memory corruption.
87 *
88 * @param <K> the type of keys maintained by this map (typically java.nio.file.Path)
89 * @param <V> the type of mapped values (typically FileAttributes)
90 * @return a synchronized, thread-safe view of a newly instantiated TreeMap
91 */
92 public static final <K, V> Map<K, V> createMap() {
93 return Collections.synchronizedSortedMap( new TreeMap<>() );
94 }
95
96 /**
97 * Lists all files in both source and destination directories concurrently using dedicated threads.
98 * <br>
99 * <br>
100 * To maximize performance on multi-core systems, this method spawns two parallel threads:
101 * One for the source path scanning and one for the destination path scanning.
102 * <br>
103 * <br>
104 * After both threads have finished execution, the provided statistics array is populated
105 * with the size and byte metrics of both maps.
106 *
107 * @param sourcePathes an ArrayList containing the base directories of the source side
108 * @param destPathes an ArrayList containing the base directories of the destination side
109 * @param stats a Long array with a minimum length of 4 used to store the tracking results:
110 * index 0: Total number of files found in source,
111 * index 1: Total number of files found in destination,
112 * index 2: Total aggregated size of source files in bytes,
113 * index 3: Total aggregated size of destination files in bytes
114 * @param deepScan the configuration determining the type and depth of the file parsing
115 * @param subDir true to recursively scan all subdirectories, false to only process the root level
116 * @param trashbin true to enable trashbin retention logic, false to bypass it
117 * @return a Map containing all paths where failures, permission issues, or structural conflicts occurred
118 */
119 public Map<Path, FileAttributes> scanSyncFiles( final List<Path> sourcePathes, final List<Path> destPathes, final Long[] stats, final ScanType deepScan, final boolean subDir,
120 final boolean trashbin ) {
121 Debug.printDebug( "[Model] list start" );
122 final Thread thread1 = new Thread( () -> handler.listFiles( sourcePathes, sourceMap, deepScan, subDir ) );
123 final Thread thread2 = new Thread( () -> handler.listFiles( destPathes, destMap, deepScan, subDir ) );
124 thread1.start();
125 thread2.start();
126 try {
127 thread1.join();
128 thread2.join();
129 }catch( final InterruptedException exception ) {
130 Debug.printException( this.getClass(), exception );
131 Thread.currentThread().interrupt();
132 }
133 stats[0] = (long) sourceMap.size();
134 stats[1] = (long) destMap.size();
135 stats[2] = getBytes( sourceMap );
136 stats[3] = getBytes( destMap );
137 Debug.printDebug( "[Model] list ready" );
138 return getFailtures( sourceMap, destMap );
139 }
140
141 /**
142 * Compares the pre-loaded source and destination maps to isolate identical files.
143 * <br>
144 * <br>
145 * This method triggers the internal handlers to filter out matching files from both
146 * maps. After execution, both maps will only contain unique entries that require
147 * synchronization actions like copy, update, or delete.
148 */
149 public void compareEqualsFiles() {
150 Debug.printDebug( "[Model] getEqualsFiles start" );
151 analyzer.equalsFiles( sourceMap, destMap );
152 Debug.printDebug( "[Model] getEqualsFiles ready" );
153 }
154
155 /**
156 * Analyzes the file state differentials to categorize synchronization requirements.
157 * <br>
158 * <br>
159 * Evaluates file modification dates, sizes, or checksums between the source and destination
160 * targets. The results are split into three structural hitlists returned as an indexed list.
161 *
162 * @param syncMap the map tracking current synchronization history states
163 * @param sourcePath the absolute base path of the source directory
164 * @param destPath the absolute base path of the destination directory
165 * @return an ArrayList containing exactly three separate maps:
166 * index 0 (copySourceHitList): Files to be copied from source to destination,
167 * index 1 (copyDestHitList): Files to be copied back from destination to source,
168 * index 2 (delHitList): Files marked for deletion from the target directory
169 */
170 public ArrayList<Map<Path, FileAttributes>> getSyncFiles( final Map<Path, FileAttributes> syncMap, final Path sourcePath, final Path destPath ) {
171 Debug.printDebug( "[Model] getSyncFiles start" );
172 final ArrayList<Map<Path, FileAttributes>> result = analyzer.getSyncFiles( sourceMap, destMap, sourcePath, destPath, syncMap );
173 Debug.printDebug( "[Model] getSyncFiles ready" );
174 return result;
175 }
176
177 /**
178 * Executes the physical file backup sequence on the local storage system.
179 * <br>
180 * <br>
181 * To ensure a clean and predictable operation, this method enforces a strict two-phase execution order:
182 * Phase 1 (Purge) clears obsolete files from the target directory first, and
183 * Phase 2 (Transfer) physically copies new or updated files into the destination path.
184 *
185 * @param delete the mode flag determining deletions (processed exclusively if set to 0)
186 * @param logOn true to output detailed file paths and transaction logs to the system logger
187 * @param destPath the absolute path to the target directory where files will be transferred to
188 * @param trashbin true to move deleted files safely into a local trash bin structure
189 * @param trashbinPath the absolute directory path representing the safe retention folder
190 * @return true if all file entries inside the tracking maps were successfully processed and cleared,
191 * false if unprocessed files remain due to operational faults or file system errors
192 */
193 public boolean backupFiles( final boolean delete, final boolean logOn, final Path destPath, final boolean trashbin, final Path trashbinPath ) {
194 if( delete && !destMap.isEmpty() ) handler.deleteFiles( destMap, logOn, trashbin, trashbinPath );
195 if( !sourceMap.isEmpty() ) handler.copyFiles( sourceMap, logOn, destPath );
196 return sourceMap.isEmpty() && destMap.isEmpty();
197 }
198
199 /**
200 * Synchronizes files bi-directionally between the configured directories.
201 * <br>
202 * <br>
203 * This function consumes the pre-calculated multi-hitlist results, transfers the newest file states,
204 * and structurally synchronizes both directories to reach an identical file state.
205 *
206 * @param result the calculated synchronization tracking lists containing the hit maps
207 * @param syncMap the map tracking current synchronization history states
208 * @param sourcePath the absolute base path of the source directory
209 * @param destPath the absolute base path of the destination directory
210 * @param testOn true to run a dry-run simulation which skips real I/O operations
211 * @return true if the entire synchronization pipeline completed without unexpected exceptions,
212 * false if errors occurred during file interaction
213 */
214 public boolean syncFiles( final SyncJobContext ctx, final ArrayList<Map<Path, FileAttributes>> result, final Map<Path, FileAttributes> syncMap, final Path sourcePath, final Path destPath,
215 final boolean testOn ) {
216 final boolean logOn = ctx.getPreference().isLogOn();
217 if( !result.get( 0 ).isEmpty() ) handler.copyFiles( result.get( 0 ), logOn, destPath );
218 if( !result.get( 1 ).isEmpty() ) handler.copyFiles( result.get( 1 ), logOn, sourcePath );
219 if( !result.get( 2 ).isEmpty() ) handler.deleteFiles( result.get( 2 ), logOn, false, null );
220
221 sourceMap.clear();
222 destMap.clear();
223 syncMap.clear();
224 if( !testOn ) {
225 final Map<Path, FileAttributes> tempMap = createMap();
226 handler.listFiles( ctx.getPreference().getSourcePaths(), tempMap, ScanType.SYNCHRONIZE, false );
227 for( final Map.Entry<Path, FileAttributes> entry : tempMap.entrySet() ) {
228 syncMap.put( entry.getValue().getRelativeFilePath(), entry.getValue() );
229 }
230 ctx.getPreference().writeSyncMap();
231 }
232 return result.get( 0 ).isEmpty() && result.get( 1 ).isEmpty() && result.get( 2 ).isEmpty();
233 }
234
235 /**
236 * Scans the selected target paths to locate and extract duplicate file structures.
237 * <br>
238 * <br>
239 * Utilizes a specialized duplicate scan analyzer that matches files based on identical
240 * parameters like sizing blocks or checksums. Any errors encountered during the filesystem
241 * traversal are collected and merged into the final state mapping.
242 *
243 * @param paths an ArrayList containing the directory paths that should be inspected for file duplicates
244 * @return a Map containing the duplicate paths mapped to their attributes, combined with failure reports
245 */
246 public Map<Path, FileAttributes> scanDublicates( final ArrayList<Path> paths, final Long... stats ) {
247 handler.listFiles( paths, sourceMap, ScanType.DUBLICATE_SCAN, false );
248 final Map<Path, FileAttributes> duplicateMap = analyzer.findDuplicates( sourceMap );
249 stats[0] = (long) sourceMap.size();
250 stats[1] = (long) getFailtures( sourceMap, destMap ).size();
251 stats[2] = 0L;
252 stats[3] = 0L;
253 return duplicateMap;
254 }
255
256 /**
257 * Aggregates processing errors, missing file attributes, or permission blocks into a dedicated failure tracking map.
258 * <br>
259 * <br>
260 * This private utility evaluates the unresolved differences between the source and destination maps
261 * after an operation has completed, isolating paths that caused structural system errors.
262 *
263 * @param sourceMap the tracking map containing the current source file information
264 * @param destMap the tracking map containing the current destination file information
265 * @return a filtered Map detailing all elements that failed to process correctly
266 */
267 private Map<Path, FileAttributes> getFailtures( final Map<Path, FileAttributes> sourceMap, final Map<Path, FileAttributes> destMap ) {
268 final Map<Path, FileAttributes> failMap = createMap();
269 final String failture = "Failed";
270 if( sourceMap != null ) {
271 for( final Map.Entry<Path, FileAttributes> entry : sourceMap.entrySet() ) {
272 if( failture.equals( entry.getValue().getFileHash() ) ) {
273 failMap.put( entry.getKey(), entry.getValue() );
274 }
275 }
276 }
277 if( destMap != null ) {
278 for( final Map.Entry<Path, FileAttributes> entry : destMap.entrySet() ) {
279 if( failture.equals( entry.getValue().getFileHash() ) ) {
280 failMap.put( entry.getKey(), entry.getValue() );
281 }
282 }
283 }
284 return failMap;
285 }
286
287 /**
288 * Calculates the total aggregated file size of all entries within the provided map.
289 * <p>
290 * Loops through the key set of paths and sums up the individual byte sizes
291 * extracted from the respective file attributes.
292 *
293 * @param map the tracking map containing the file paths and their associated attributes
294 * @return the total size of all files combined, represented in bytes
295 */
296 private Long getBytes( final Map<Path, FileAttributes> map ) {
297 long allBytes = 0;
298 for( final FileAttributes p : map.values() ) {
299 allBytes += p.getSize();
300 }
301 return allBytes;
302 }
303 }