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