@@ -76,15 +76,16 @@ public void writesSeparateDraftAndMetadataAndLoadsThem() throws Exception {
7676 assertNotNull (loaded );
7777 assertEquals ("recover me" , loaded .text );
7878 assertEquals (3 , loaded .metadata .generation );
79- assertTrue (new File (repository .getDirectoryForTests (), key + ".draft" ).isFile ());
80- assertTrue (new File (repository .getDirectoryForTests (), key + ".json" ).isFile ());
79+ File generation = publishedGeneration (key );
80+ assertTrue (new File (generation , "draft.draft" ).isFile ());
81+ assertTrue (new File (generation , "metadata.json" ).isFile ());
8182 }
8283
8384 @ Test
8485 public void invalidLengthAndMissingPairAreRejected () throws Exception {
8586 String key = RecoveryKeys .forUntitledDocument ();
8687 repository .write (metadata (key , null , 1 ), "valid" );
87- File draft = new File (repository . getDirectoryForTests ( ), key + " .draft" );
88+ File draft = new File (publishedGeneration ( key ), "draft .draft" );
8889 try (FileOutputStream output = new FileOutputStream (draft , true )) {
8990 output .write ('x' );
9091 }
@@ -98,7 +99,7 @@ public void invalidLengthAndMissingPairAreRejected() throws Exception {
9899 public void sameLengthContentMismatchIsRejected () throws Exception {
99100 String key = RecoveryKeys .forUntitledDocument ();
100101 repository .write (metadata (key , null , 1 ), "first" );
101- File draft = new File (repository . getDirectoryForTests ( ), key + " .draft" );
102+ File draft = new File (publishedGeneration ( key ), "draft .draft" );
102103 try (FileOutputStream output = new FileOutputStream (draft , false )) {
103104 output .write ("other" .getBytes (StandardCharsets .UTF_8 ));
104105 }
@@ -110,7 +111,7 @@ public void sameLengthContentMismatchIsRejected() throws Exception {
110111 public void staleActivePointerIsCleared () throws Exception {
111112 String key = RecoveryKeys .forUntitledDocument ();
112113 repository .write (metadata (key , null , 1 ), "draft" );
113- new File ( repository . getDirectoryForTests (), key + ".draft" ). delete ( );
114+ deleteRecursively ( publishedGeneration ( key ) );
114115
115116 assertNull (repository .loadActive ());
116117 assertNull (ApplicationProvider .<Context >getApplicationContext ()
@@ -140,8 +141,49 @@ public void deleteRemovesPairAndActivePointer() throws Exception {
140141 repository .delete (key );
141142
142143 assertNull (repository .load (key , null ));
143- assertFalse (new File (repository .getDirectoryForTests (), key + ".draft" ).exists ());
144- assertFalse (new File (repository .getDirectoryForTests (), key + ".json" ).exists ());
144+ assertFalse (new File (repository .getDirectoryForTests (), key + ".current" ).exists ());
145+ assertFalse (new File (repository .getDirectoryForTests (), key + ".generations" ).exists ());
146+ }
147+
148+ @ Test
149+ public void incompleteUnpublishedGenerationDoesNotReplaceCurrentDraft () throws Exception {
150+ String key = RecoveryKeys .forUntitledDocument ();
151+ repository .write (metadata (key , null , 1 ), "previous" );
152+ File generations = new File (repository .getDirectoryForTests (), key + ".generations" );
153+ File incomplete = new File (generations , "generation-interrupted.tmp" );
154+ assertTrue (incomplete .mkdir ());
155+ try (FileOutputStream output = new FileOutputStream (new File (incomplete , "draft.draft" ))) {
156+ output .write ("newer" .getBytes (StandardCharsets .UTF_8 ));
157+ }
158+
159+ repository .cleanupIncompleteArtifacts ();
160+
161+ RecoveryDraft draft = repository .loadActive ();
162+ assertNotNull (draft );
163+ assertEquals ("previous" , draft .text );
164+ assertFalse (incomplete .exists ());
165+ }
166+
167+ @ Test
168+ public void cleanupRestoresLegacyBackupBeforeLoading () throws Exception {
169+ String key = RecoveryKeys .forUntitledDocument ();
170+ RecoveryMetadata published = metadata (key , null , 1 ).published (
171+ "valid" .getBytes (StandardCharsets .UTF_8 ).length ,
172+ sha256 ("valid" .getBytes (StandardCharsets .UTF_8 )),
173+ 123L
174+ );
175+ File directory = repository .getDirectoryForTests ();
176+ assertTrue (directory .mkdirs ());
177+ write (new File (directory , key + ".draft" ), "broken" );
178+ write (new File (directory , key + ".draft.bak" ), "valid" );
179+ write (new File (directory , key + ".json" ), published .toJson ().toString ());
180+
181+ repository .cleanupIncompleteArtifacts ();
182+
183+ RecoveryDraft draft = repository .load (key , null );
184+ assertNotNull (draft );
185+ assertEquals ("valid" , draft .text );
186+ assertFalse (new File (directory , key + ".draft.bak" ).exists ());
145187 }
146188
147189 private RecoveryMetadata metadata (String key , String uri , long generation ) {
@@ -151,6 +193,29 @@ private RecoveryMetadata metadata(String key, String uri, long generation) {
151193 );
152194 }
153195
196+ private File publishedGeneration (String key ) {
197+ File generations = new File (repository .getDirectoryForTests (), key + ".generations" );
198+ File [] children = generations .listFiles (file -> file .isDirectory () && !file .getName ().endsWith (".tmp" ));
199+ assertNotNull (children );
200+ assertEquals (1 , children .length );
201+ return children [0 ];
202+ }
203+
204+ private static void write (File file , String value ) throws Exception {
205+ try (FileOutputStream output = new FileOutputStream (file )) {
206+ output .write (value .getBytes (StandardCharsets .UTF_8 ));
207+ }
208+ }
209+
210+ private static String sha256 (byte [] value ) throws Exception {
211+ java .security .MessageDigest digest = java .security .MessageDigest .getInstance ("SHA-256" );
212+ StringBuilder result = new StringBuilder ();
213+ for (byte item : digest .digest (value )) {
214+ result .append (String .format (java .util .Locale .ROOT , "%02x" , item & 0xff ));
215+ }
216+ return result .toString ();
217+ }
218+
154219 private static void deleteRecursively (File file ) {
155220 if (!file .exists ()) {
156221 return ;
0 commit comments