11package pl .skidam .automodpack_loader_core .client ;
22
3+ import org .jetbrains .annotations .NotNull ;
34import pl .skidam .automodpack_core .auth .Secrets ;
45import pl .skidam .automodpack_core .config .ConfigTools ;
56import pl .skidam .automodpack_core .config .Jsons ;
1617import java .io .*;
1718import java .net .*;
1819import java .nio .file .*;
20+ import java .nio .file .attribute .BasicFileAttributes ;
1921import java .security .cert .CertificateEncodingException ;
2022import java .security .cert .X509Certificate ;
2123import java .util .*;
@@ -50,40 +52,93 @@ public static UpdateCheckResult isUpdate(Jsons.ModpackContentFields serverModpac
5052 return new UpdateCheckResult (true , serverModpackContent .list );
5153 }
5254
53- LOGGER .info ("Indexing file system..." );
54- var start = System .currentTimeMillis ();
55-
56- Set <Path > existingFileTree ;
57- try (var stream = Files .walk (modpackDir )) {
58- existingFileTree = stream .collect (Collectors .toSet ());
59- } catch (IOException e ) {
60- LOGGER .error ("Failed to walk directory" , e );
61- return new UpdateCheckResult (true , serverModpackContent .list );
62- }
63-
6455 LOGGER .info ("Verifying content against server list..." );
56+ var start = System .currentTimeMillis ();
6557
6658 Set <Jsons .ModpackContentFields .ModpackContentItem > filesToUpdate = ConcurrentHashMap .newKeySet ();
6759
60+ // Group & Sort Server Files (Optimizes Disk Seek Pattern)
61+ // Grouping by parent folder ensures we process the disk sequentially (Dir A, then Dir B).
62+ // TreeMap ensures alphabetical order of directories (HDD friendly).
63+ Map <Path , List <Jsons .ModpackContentFields .ModpackContentItem >> itemsByDir =
64+ serverModpackContent .list .stream ()
65+ .collect (Collectors .groupingBy (
66+ item -> SmartFileUtils .getPath (modpackDir , item .file ).getParent (),
67+ TreeMap ::new ,
68+ Collectors .toList ()
69+ ));
70+
6871 try (var cache = FileMetadataCache .open (hashCacheDBFile )) {
69- serverModpackContent .list .forEach (serverItem -> {
70- Path serverItemPath = SmartFileUtils .getPath (modpackDir , serverItem .file );
71- if (!existingFileTree .contains (serverItemPath )) {
72- filesToUpdate .add (serverItem ); // File is missing
73- return ;
74- } else if (serverItem .editable ) { // TODO check if this is enough of a check, what if user already had a file but there's provided the same by a new modpack version which wasnt in the modpack before?
75- LOGGER .debug ("Skipping editable file hash check: {}" , serverItem .file );
76- return ;
72+
73+ // Process Directory by Directory
74+ for (Map .Entry <Path , List <Jsons .ModpackContentFields .ModpackContentItem >> entry : itemsByDir .entrySet ()) {
75+ Path parentDir = entry .getKey ();
76+ List <Jsons .ModpackContentFields .ModpackContentItem > itemsInDir = entry .getValue ();
77+
78+ // If directory is missing, all items in it are missing.
79+ if (!Files .exists (parentDir )) {
80+ filesToUpdate .addAll (itemsInDir );
81+ continue ;
7782 }
7883
79- String hash = cache .getHashOrNull (serverItemPath );
80- if (hash != null && hash .equals (serverItem .sha1 )) {
81- return ; // File is up to date
84+ // Read all file attributes in this folder in ONE pass.
85+ // This map will hold "FileName" -> "Attributes"
86+ Map <String , BasicFileAttributes > diskFiles = new HashMap <>();
87+
88+ try {
89+ // walkFileTree with depth 1 is efficient on Windows (gets attributes for free within a single syscall)
90+ Files .walkFileTree (parentDir , EnumSet .noneOf (FileVisitOption .class ), 1 , new SimpleFileVisitor <>() {
91+ @ NotNull @ Override
92+ public FileVisitResult visitFile (@ NotNull Path file , @ NotNull BasicFileAttributes attrs ) {
93+ diskFiles .put (file .getFileName ().toString (), attrs );
94+ return FileVisitResult .CONTINUE ;
95+ }
96+
97+ @ NotNull @ Override
98+ public FileVisitResult visitFileFailed (@ NotNull Path file , @ NotNull IOException exc ) {
99+ return FileVisitResult .CONTINUE ; // Handle locked files or permission errors gracefully
100+ }
101+ });
102+ } catch (IOException e ) {
103+ LOGGER .warn ("Failed to inspect directory: {}" , parentDir , e );
104+ filesToUpdate .addAll (itemsInDir );
105+ continue ;
82106 }
83107
84- // This file needs to be updated
85- filesToUpdate .add (serverItem );
86- });
108+ // Check Individual Files in a given directory (Pure RAM logic, 0 IO)
109+ for (var serverItem : itemsInDir ) {
110+ String fileName = Paths .get (serverItem .file ).getFileName ().toString ();
111+ BasicFileAttributes diskAttrs = diskFiles .get (fileName );
112+
113+ if (diskAttrs == null ) {
114+ // File does not exist in the directory map
115+ filesToUpdate .add (serverItem );
116+ } else {
117+ if (serverItem .editable ) { // TODO check if this is enough of a check, what if user already had a file but there's provided the same by a new modpack version which wasn't in the modpack before?
118+ LOGGER .debug ("Skipping editable file hash check: {}" , serverItem .file );
119+ continue ;
120+ }
121+
122+ // Check Size first from already read attributes
123+ if (diskAttrs .size () != Long .parseLong (serverItem .size )) {
124+ filesToUpdate .add (serverItem );
125+ continue ;
126+ }
127+
128+ // Finally, check Hash
129+ // We pass 'diskAttrs' to the cache so it doesn't need to re-stat the file.
130+ String hash = cache .getHashOrNullWithAttributes (parentDir .resolve (fileName ), diskAttrs );
131+
132+ if (!serverItem .sha1 .equalsIgnoreCase (hash )) {
133+ filesToUpdate .add (serverItem );
134+ }
135+ }
136+ }
137+ }
138+ } catch (Exception e ) {
139+ LOGGER .error ("Error during update check" , e );
140+ // Fail-safe: assume update needed if process crashes
141+ return new UpdateCheckResult (true , serverModpackContent .list );
87142 }
88143
89144 if (!filesToUpdate .isEmpty ()) {
@@ -99,7 +154,7 @@ public static UpdateCheckResult isUpdate(Jsons.ModpackContentFields serverModpac
99154
100155 for (Jsons .ModpackContentFields .ModpackContentItem clientItem : clientModpackContent .list ) {
101156 if (!serverFileSet .contains (clientItem .file )) {
102- LOGGER .info ("Found file marked for deletion (its no longer on server) : {}" , clientItem .file );
157+ LOGGER .info ("Found file marked for deletion: {}" , clientItem .file );
103158 return new UpdateCheckResult (true , Set .of ());
104159 }
105160 }
0 commit comments