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 }