View Javadoc
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 }