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.io.IOException;
24  import java.nio.file.Files;
25  import java.nio.file.LinkOption;
26  import java.nio.file.Path;
27  import java.nio.file.StandardCopyOption;
28  import java.nio.file.attribute.DosFileAttributeView;
29  import java.nio.file.attribute.FileTime;
30  import java.nio.file.attribute.PosixFileAttributeView;
31  import java.nio.file.attribute.PosixFilePermission;
32  import java.util.EnumSet;
33  import java.util.List;
34  import java.util.Map;
35  import java.util.Set;
36  import java.util.concurrent.ExecutorService;
37  import java.util.concurrent.Executors;
38  import java.util.concurrent.TimeUnit;
39  
40  import de.spiritscorp.datasync.ScanType;
41  import de.spiritscorp.datasync.io.Debug;
42  import de.spiritscorp.datasync.io.Logger;
43  
44  /**
45   * Component responsible for handling low-level file operations such as listing,
46   * copying, and deleting files across various storage backends.
47   */
48  class FileHandler {
49  
50  	/** The logger instance used for system diagnostics and error reporting. */
51  	private final Logger log;
52  
53  	/**
54  	 * Constructs a new FileHandler with the specified logger.
55  	 *
56  	 * @param logger the logger instance to be used for diagnostic output
57  	 */
58  	FileHandler( final Logger logger ) {
59  		this.log = logger;
60  	}
61  
62  	/**
63  	 * Scans the provided directory paths asynchronously to collect file attributes.
64  	 * <br>
65  	 * <br>
66  	 * The results are populated directly into the provided thread-safe result map.
67  	 * This method supports infinite processing loops to safely handle slow network shares.
68  	 *
69  	 * @param paths     the list of root directory paths to scan
70  	 * @param resultMap the shared map where resolved file paths and attributes are stored
71  	 * @param deepScan  the strategy determining the depth and thoroughness of the file analysis
72  	 * @param subDir    {@code true} to recursively traverse subdirectories, {@code false} otherwise
73  	 */
74  	void listFiles( final List<Path> paths, final Map<Path, FileAttributes> resultMap, final ScanType deepScan, final boolean subDir ) {
75  		try( ExecutorService executor = Executors.newSingleThreadExecutor() ) {
76  			for( final Path path : paths ) {
77  				// Guard: Check interruption context before entering the file tree walker system
78  				if( Thread.currentThread().isInterrupted() ) {
79  					Debug.printDebug( "[File Handler] Interrupt!  Interruption detected prior to walking directory path: %s", path.toString() );
80  					executor.shutdownNow();
81  					return;
82  				}
83  				if( Files.exists( path ) ) {
84  					walkTree( path.normalize(), executor, resultMap, deepScan, subDir );
85  				}
86  			}
87  			executor.shutdown();
88  			while( !executor.awaitTermination( 100, TimeUnit.MILLISECONDS ) ) {
89  				if( Thread.currentThread().isInterrupted() ) {
90  					executor.shutdownNow();
91  					return;
92  				}
93  			}
94  		}catch( InterruptedException _ ) {
95  			Debug.printDebug( "[File Handler] File processing walk subsystem was forcefully interrupted." );
96  			Thread.currentThread().interrupt();
97  		}
98  		for( final Path path : paths ) {
99  			Debug.printDebug( "[File Handler] ListFiles() -> ready  %s -> %s", Thread.currentThread().getName(), path.toString() );
100 		}
101 	}
102 
103 	/**
104 	 * Deletes files from the specified map with optional trashbin backup.
105 	 *
106 	 * <p>For each file in the map:
107 	 * <ol>
108 	 * <li>Optionally copies file to trashbin directory before deletion</li>
109 	 * <li>Sets write permission if needed</li>
110 	 * <li>Deletes the file</li>
111 	 * <li>Logs the operation result</li>
112 	 * <ol>
113 	 * <p>
114 	 *
115 	 * @param map          Map of files to delete
116 	 * @param logOn        If true, prints status after completion
117 	 * @param trashbin     If true, copies files to trashbin before deletion
118 	 * @param trashbinPath Path to trashbin directory
119 	 */
120 	void deleteFiles( final Map<Path, FileAttributes> map, final boolean logOn, final boolean trashbin, final Path trashbinPath ) {
121 		for( final Map.Entry<Path, FileAttributes> entry : map.entrySet() ) {
122 			final FileAttributes fileAttr = entry.getValue();
123 			final Path path = entry.getKey();
124 			// Guard: Check thread interrupt status before executing file operations
125 			if( Thread.currentThread().isInterrupted() ) {
126 				Debug.printDebug( "[File Handler] Interrupt! Safe loop interruption caught within file deletion loop vector at: %s", path.toString() );
127 				break;
128 			}else if( fileAttr == null ) continue;
129 			processSingleDeletion( path, fileAttr, trashbin, trashbinPath );
130 		}
131 		map.clear();
132 		if( logOn ) log.printStatus();
133 	}
134 
135 	/**
136 	 * Copies files from source to destination preserving file attributes.
137 	 *
138 	 * <p>For each file in the map:
139 	 * <ol>
140 	 * <li>Creates parent directories if needed</li>
141 	 * <li>Sets write permission on existing destination if needed</li>
142 	 * <li>Copies file with REPLACE_EXISTING and COPY_ATTRIBUTES options</li>
143 	 * <li>Restores original creation time</li>
144 	 * <li>Logs the operation result</li>
145 	 * <ol>
146 	 * <p>
147 	 *
148 	 * @param map      Map of files to copy (key=source path, value=file attributes)
149 	 * @param logOn    If true, prints status after completion
150 	 * @param destPath Destination directory path
151 	 */
152 	void copyFiles( final Map<Path, FileAttributes> map, final boolean logOn, final Path destPath ) {
153 		for( final Map.Entry<Path, FileAttributes> entry : map.entrySet() ) {
154 			// Guard: Check thread interrupt status before starting next copy transaction step
155 			if( Thread.currentThread().isInterrupted() ) {
156 				Debug.printDebug( "[File Handler] Interrupt! Safe loop interruption caught within file replication loop vector at: %s", entry.getKey().toString() );
157 				break;
158 			}
159 			final FileAttributes fileAttr = entry.getValue();
160 			if( fileAttr == null ) continue;
161 			final Path path = destPath.resolve( fileAttr.getRelativeFilePath() );
162 
163 			if( ensureWritable( path ) ) {
164 				if( moveFile( entry.getKey(), path, fileAttr.getCreateTime() ) ) {
165 					log.setEntry( path.toString(), "kopiert", fileAttr );
166 				}else {
167 					log.setEntry( path.toString(), "FEHLER BEIM KOPIEREN", fileAttr );
168 					Debug.printDebug( "[File Handler Error] Copy failed: %s", path.toString() );
169 				}
170 			}else {
171 				log.setEntry( path.toString(), "SCHREIBSCHUTZ BEIM KOPIEREN", fileAttr );
172 				Debug.printDebug( "[File Handler Error] Copy failed, target file is not writable: %s", path.toString() );
173 			}
174 		}
175 		map.clear();
176 		if( logOn ) log.printStatus();
177 	}
178 
179 	/**
180 	 * Orchestrates the deletion lifecycle for a single tracked file asset within the pipeline.
181 	 * <br>
182 	 * <br>
183 	 * If the staging flag is active, the method attempts to safely relocate the file to the designated trash bin
184 	 * architecture before purging it from the source node. It dynamically intercepts read-only locks by invoking
185 	 * permission elevation routines, ensuring that individual file transaction failures or hardware anomalies
186 	 * are captured defensively and recorded without halting the collective loop.
187 	 *
188 	 * @param path         the absolute file system path node of the file target to be deleted
189 	 * @param fileAttr     the architectural metadata container mapping the historical context of the file asset
190 	 * @param trashbin     controls whether the file should be moved into a backup staging area prior to removal
191 	 * @param trashbinPath the root destination directory layer representing the virtual trash bin storage pool
192 	 */
193 	private void processSingleDeletion( final Path path, final FileAttributes fileAttr, final boolean trashbin, final Path trashbinPath ) {
194 		try {
195 			if( trashbin && trashbinPath != null ) {
196 				final Path trashbinFile = trashbinPath.resolve( fileAttr.getRelativeFilePath() );
197 				if( !moveFile( path, trashbinFile, fileAttr.getCreateTime() ) ) {
198 					log.setEntry( path.toString(), "FEHLER BEIM VERSCHIEBEN IN DEN PAPIERKORB", fileAttr );
199 					Debug.printDebug( "[File Handler Error] Copy failed SourcePath :%s  DestPath: %s", path.toString(), trashbinFile.toString() );
200 				}
201 			}
202 			if( ensureWritable( path ) ) {
203 				Files.delete( path );
204 				log.setEntry( path.toString(), "gelöscht", fileAttr );
205 			}else {
206 				log.setEntry( path.toString(), "SCHREIBSCHUTZ BEIM LÖSCHEN", fileAttr );
207 				Debug.printDebug( "[File Handler Error] Delete failed, target file is not writable: %s", path.toString() );
208 			}
209 		}catch( final IOException exception ) {
210 			log.setEntry( path.toString(), "FEHLER BEIM LÖSCHEN", fileAttr );
211 			Debug.printDebug( "[File Handler Error] Delete failed: %s", path.toString() );
212 			Debug.printException( this.getClass(), exception );
213 		}
214 	}
215 
216 	/**
217 	 * Evaluates and dynamically elevates file system privileges to ensure the target path is writable.
218 	 * <br>
219 	 * <br>
220 	 * This method utilizes a robust feature-detection pattern to achieve pure platform independence, bypassing
221 	 * fragile operating system string checks. If the file is not natively writable, it sequentially polls the
222 	 * layout for {@link DosFileAttributeView} (Windows/NTFS) or {@link PosixFileAttributeView} (macOS/Linux/Jimfs).
223 	 * POSIX permission modifications are designed to be additive, appending the owner write bit to a copy of the
224 	 * existing permission mask to guarantee that file readability states remain entirely intact.
225 	 *
226 	 * @param path the target path node evaluated and cleared for structural write operations
227 	 * @return {@code true} if the file is verified as writable or was successfully elevated to a writable state;
228 	 *         {@code false} if permissions could not be altered or the underlying file system capabilities are unsupported
229 	 */
230 	private boolean ensureWritable( final Path path ) {
231 		if( !Files.exists( path, LinkOption.NOFOLLOW_LINKS ) ) return true;
232 		if( Files.isWritable( path ) ) return true;
233 		try {
234 
235 			// 1. Check for DOS capability (Windows, FAT32/NTFS external drives)
236 			final DosFileAttributeView dosView = Files.getFileAttributeView( path, DosFileAttributeView.class, LinkOption.NOFOLLOW_LINKS );
237 			if( dosView != null ) {
238 				dosView.setReadOnly( false );
239 				return true;
240 			}
241 
242 			// 2. Check for POSIX capability (UNIX, macOS, Jimfs Unix environment)
243 			final PosixFileAttributeView posixView = Files.getFileAttributeView( path, PosixFileAttributeView.class, LinkOption.NOFOLLOW_LINKS );
244 			if( posixView != null ) {
245 				final Set<PosixFilePermission> permissions = EnumSet.noneOf( PosixFilePermission.class );
246 				permissions.addAll( posixView.readAttributes().permissions() );
247 				if( permissions.add( PosixFilePermission.OWNER_WRITE ) ) {
248 					posixView.setPermissions( permissions );
249 					return true;
250 				}
251 			}
252 			// 3. Fallback if the filesystem supports neither view
253 			Debug.printDebug( "[File Handler] File system operations are not supported for this specific target track: %s", path.toString() );
254 		}catch( final IOException exception ) {
255 			Debug.printDebug( "[File Handler Error] I/O error, file permissions could not be verified or modified: %s", exception.getLocalizedMessage() );
256 			Debug.printException( this.getClass(), exception );
257 		}
258 		return false;
259 	}
260 
261 	/**
262 	 * Replicates a file asset across system tracking layouts while synchronizing underlying temporal attributes.
263 	 * <br>
264 	 * <br>
265 	 * This private helper encapsulates defensive verification steps. It guarantees idempotent execution by
266 	 * automatically generating any missing parent directory trees on the fly. Upon successful stream replication
267 	 * using standard replacement options, it forcefully overwrites the target's metadata cluster to ensure the
268 	 * origin file creation timestamp is structurally preserved.
269 	 *
270 	 * @param sourceFilePath the absolute source path node providing the active data payload stream
271 	 * @param destFilePath   the targeted destination path node location where the synchronized file asset will materialize
272 	 * @param createTime     the original creation timestamp token used to override the final destination file attributes
273 	 * @return {@code true} if the file transfer and attribute injection sequences finish successfully;
274 	 *         {@code false} if blocked by an internal I/O failure, null references, or file-locking collision states
275 	 */
276 	private boolean moveFile( final Path sourceFilePath, final Path destFilePath, final FileTime createTime ) {
277 		if( destFilePath == null || sourceFilePath == null ) return false;
278 		try {
279 			if( !Files.exists( destFilePath ) ) Files.createDirectories( destFilePath.getParent() );
280 			Files.copy( sourceFilePath, destFilePath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES );
281 			Files.setAttribute( destFilePath, "creationTime", createTime );
282 			return true;
283 		}catch( IOException exception ) {
284 			Debug.printException( this.getClass(), exception );
285 			return false;
286 		}
287 	}
288 
289 	/**
290 	 * Initiates a synchronous, deep file tree traversal starting at the given path.
291 	 * <br>
292 	 * <br>
293 	 * Any structural I/O errors encountered during the walk are caught locally
294 	 * and sent to the debug subsystem to prevent the entire batch scan from failing.
295 	 *
296 	 * @param path      the root directory path where the traversal begins
297 	 * @param executor  the executor service processing asynchronous file task attributes
298 	 * @param resultMap the shared map where discovered paths and attributes are registered
299 	 * @param deepScan  the strategy determining the thoroughness of the file analysis
300 	 * @param subDir    {@code true} to use the parent directory as the base context,
301 	 *                  {@code false} to use the path itself
302 	 */
303 	private void walkTree( final Path path, final ExecutorService executor, final Map<Path, FileAttributes> resultMap, final ScanType deepScan, final boolean subDir ) {
304 		try {
305 			final Path baseDir = subDir ? path.getParent() : path;
306 			Files.walkFileTree( path, new FileVisit( executor, baseDir, resultMap, deepScan ) );
307 		}catch( final IOException exception ) {
308 			Debug.printDebug( "[File Handler Error] Error on walking directory path: %s with message: %s", path.toString(), exception.getMessage() );
309 			Debug.printException( this.getClass(), exception );
310 		}
311 	}
312 
313 }