-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathApposeUtils.java
More file actions
308 lines (285 loc) · 11.1 KB
/
ApposeUtils.java
File metadata and controls
308 lines (285 loc) · 11.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
/*-
* #%L
* mastodon-deep-lineage
* %%
* Copyright (C) 2022 - 2025 Stefan Hahmann
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
* #L%
*/
package org.mastodon.mamut.util.appose;
import java.awt.Component;
import java.io.File;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Set;
import javax.annotation.Nonnull;
import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;
import org.apache.commons.lang3.tuple.Pair;
import org.apposed.appose.Builder;
import org.apposed.appose.builder.Builders;
import org.apposed.appose.util.Environments;
import org.mastodon.mamut.util.ByteFormatter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ApposeUtils
{
private ApposeUtils()
{
// prevent instantiation
}
private static final Logger logger = LoggerFactory.getLogger( MethodHandles.lookup().lookupClass() );
/**
* Installs a Python environment based on the provided content inside the appose environments directory.
* Use mamba to handle the installation process. The content should be in YAML format.
*
* @param envContent The YAML content that defines the Python environment configuration.
* This should include dependencies and other necessary details
* to set up the environment.
* @throws IOException If an I/O error occurs during the installation process.
*/
public static void installEnvironment(final String envContent, final Builder<?> envBuilder) throws IOException
{
envBuilder.content( envContent ).logDebug().rebuild();
}
/**
* Deletes the specified Python environment and all its associated files from the appose environments directory.
* If the deletion fails, an error message is logged and displayed in a dialog box.
*
* @param envName The name of the environment to be deleted. This corresponds to the directory name in the appose environments directory.
* @param parent The parent component for displaying any error dialog in case the deletion fails.
*/
public static void deleteEnvironment( final String envName, final Component parent )
{
File envDir = new File( Environments.apposeEnvsDir(), envName );
try
{
deleteDirectory( envDir );
}
catch ( IOException e )
{
logger.error( "Deletion failed for environment: {}. Reason: {}", envName, e.getMessage(), e );
SwingUtilities.invokeLater( () -> JOptionPane.showMessageDialog(
parent,
"Could not delete directory " + envDir + ". Reason: " + e.getMessage(),
"Error",
JOptionPane.ERROR_MESSAGE
) );
}
}
/**
* Checks if a specific Python environment is installed in the appose environments directory.
* The method verifies the existence of the environment by checking if the directory
* corresponding to the environment name can be wrapped or accessed.
*
* @param envName The name of the environment to check. This should match the directory
* name inside the appose environments directory.
* @return true if the environment is installed and accessible, false otherwise.
*/
public static boolean checkEnvironmentInstalled( final String envName )
{
return Builders.canWrap( new File( Environments.apposeEnvsDir(), envName ) );
}
/**
* Calculates the total size of a specified python environment managed by appose and returns the size
* as a human-readable string. If the directory does not exist, it returns "N/A".
*
* @param envName The name of the environment directory to calculate the size for.
* @return The size of the specified environment directory as a human-readable string,
* or "N/A" if the directory does not exist.
*/
public static String calculateEnvironmentSize( final String envName )
{
File envDir = new File( Environments.apposeEnvsDir(), envName );
if ( !envDir.exists() )
return "N/A";
long size = calculateDirectorySize( envDir );
return ByteFormatter.humanReadableByteCount( size );
}
/**
* Deletes the specified directory and all its contents, including subdirectories and files.<br>
* This method performs a recursive deletion of the directory and its contents. If the directory
* does not exist or is null, the method does nothing.
*
* @param dir The directory to be deleted. If null or does not exist, the method will return without any action.
* @throws IOException If an I/O error occurs while deleting files or directories.
*/
public static void deleteDirectory( final File dir ) throws IOException
{
if ( dir == null || !dir.exists() )
return;
File[] files = dir.listFiles();
if ( files != null )
{
for ( File file : files )
{
if ( file.isDirectory() )
deleteDirectory( file );
else
Files.deleteIfExists( file.toPath() );
}
}
Files.deleteIfExists( dir.toPath() );
}
/**
* Calculates the total size of a directory, including all files and subdirectories.
*
* @param directory The directory whose size is to be calculated. Must not be null and must represent a valid directory.
* @return The total size of the directory in bytes. If the directory does not exist or is null, the method will return 0.
*/
public static long calculateDirectorySize( final File directory )
{
if ( directory == null || !directory.exists() )
{
return 0;
}
String os = System.getProperty( "os.name" ).toLowerCase();
boolean isUnix = os.contains( "nix" ) || os.contains( "nux" ) || os.contains( "mac" );
if ( isUnix )
{
try
{
return calculateDiskUsageUnix( directory.toPath() );
}
catch ( IOException e )
{
// fallback to generic method if attribute not supported
return calculateFileLengthRecursive( directory );
}
}
else
{
return calculateFileLengthRecursive( directory );
}
}
// --- Linux/macOS: actual disk usage, like du -sb ---
private static long calculateDiskUsageUnix( final Path directory ) throws IOException
{
final long[] total = { 0 };
boolean hasUnixView = supportsUnixAttributes();
Files.walkFileTree( directory, new SimpleFileVisitor< Path >()
{
@Nonnull
@Override
public FileVisitResult preVisitDirectory( @Nonnull Path dir, @Nonnull BasicFileAttributes attrs ) throws IOException
{
return Files.isSymbolicLink( dir )
? FileVisitResult.SKIP_SUBTREE
: FileVisitResult.CONTINUE;
}
@Nonnull
@Override
public FileVisitResult visitFile( @Nonnull Path file, @Nonnull BasicFileAttributes attrs ) throws IOException
{
if ( !Files.isSymbolicLink( file ) )
total[ 0 ] += getFileDiskUsage( file, hasUnixView );
return FileVisitResult.CONTINUE;
}
} );
return total[ 0 ];
}
private static boolean supportsUnixAttributes()
{
Set< String > views = FileSystems.getDefault().supportedFileAttributeViews();
return views.contains( "unix" );
}
/**
* Returns the disk usage (in bytes) for a single file.
* Uses actual allocated blocks if the unix view is supported.
*/
private static long getFileDiskUsage( final Path file, final boolean hasUnixView ) throws IOException
{
if ( !hasUnixView )
return Files.size( file );
try
{
Object blocksAttr = Files.getAttribute( file, "unix:blocks", LinkOption.NOFOLLOW_LINKS );
if ( blocksAttr instanceof Number )
return ( ( Number ) blocksAttr ).longValue() * 512L; // du uses 512-byte blocks
}
catch ( IllegalArgumentException | UnsupportedOperationException e )
{
// ignore and fall back to logical size
}
return Files.size( file );
}
// --- Windows / fallback: logical file sizes only ---
private static long calculateFileLengthRecursive( final File directory )
{
long size = 0;
File[] files = directory.listFiles();
if ( files != null )
{
for ( File file : files )
{
if ( Files.isSymbolicLink( file.toPath() ) )
continue;
if ( file.isFile() )
size += file.length();
else if ( file.isDirectory() )
size += calculateFileLengthRecursive( file );
}
}
return size;
}
/**
* Checks whether a specific Python environment is installed and prompts the user to install it
* if it is not already installed. The user is presented with a dialog box to confirm the installation.
* If the user declines, the method a pair consisting of {@code false} and an appropriate message.
*
* @param envName The name of the environment to check. This should match the directory
* name inside the appose environments directory.
* @param log The logger instance used for logging messages during the environment check
* and installation process.
* @return A pair where the first element is a boolean indicating if the environment is installed
* or was successfully triggered for installation, and the second element is a message
* providing additional information about the process or failure reason.
*/
public static Pair< Boolean, String > confirmEnvInstallation( final String envName, final org.scijava.log.Logger log )
{
boolean isEnvInstalled = checkEnvironmentInstalled( envName );
if ( !isEnvInstalled )
{
log.info( "Python environment not yet installed, but can be installed now.\n" );
int result = JOptionPane.showConfirmDialog(
null,
"The required Python environment is not installed.\nWould you like to install it now?\n\nNote: This requires an internet connection and may take several minutes.",
"Python Environment Installation",
JOptionPane.YES_NO_OPTION,
JOptionPane.QUESTION_MESSAGE
);
if ( result != JOptionPane.YES_OPTION )
return Pair.of( false, "Python environment installation was declined.\nCannot proceed without the required environment." );
log.info( "User confirmed installation. Installing Python environment.\n" );
log.info( "Installation progress can be observed using FIJI console: FIJI > Window > Console.\n" );
}
return Pair.of( true, "" );
}
}